9.4 KiB
OpenCode Commit Message Stdin Delivery
Problem
OpenCode's preset Source Control AI path passes the full generated prompt as the final opencode run argv positional. Commit-message prompts include branch, staged file summary, user instructions, and the staged patch. The patch is capped by STAGED_DIFF_BYTE_BUDGET = 200_000, but the implementation uses JavaScript string length, not encoded byte length, so argv can still be much larger than command-line limits on Windows and some POSIX/SSH wrapper paths before OpenCode starts.
Verified current code:
src/shared/commit-message-agent-spec.ts:24-32definespromptDeliveryas the argv/stdin contract for large diffs.src/shared/commit-message-agent-spec.ts:322-337sets OpenCode topromptDelivery: 'argv'and appendsprompt.src/shared/commit-message-plan.ts:108-123already routespromptDelivery === 'stdin'intostdinPayload.src/main/text-generation/commit-message-text-generation.ts:470-601pipes localstdinPayloadto child stdin.src/main/providers/ssh-git-provider.ts:122-140forwardsstdinPayloadas relaystdin;src/relay/agent-exec-handler.ts:160-303writes it to the remote child.src/shared/commit-message-plan.test.ts:36currently locks in the broken OpenCode argv behavior.
Feasibility Gate
opencode --version on the local machine reports 1.15.13. opencode run --help confirms the required args exist: --model, --agent, --format, and --variant, with optional [message..] positionals. It does not document stdin prompt input.
Shipping gate: before changing the preset, run a controlled smoke test using the exact no-message shape, and verify stdout clearly reflects the stdin prompt rather than an empty task:
printf '%s\n' 'Write: test commit' | opencode run --model <model> --agent build --format default
Also smoke --variant <level> if the selected model exposes thinking levels. If credentials, model access, or an unsupported OpenCode version block validation, do not treat --help alone as proof. If stdin fails, do not add an argv fallback; stop and choose a different no-argv transport or version-gated behavior.
Root Cause
The planner and both local/SSH execution paths already know how to carry stdin payloads. The mismatch is the OpenCode preset args.
OpenCode is not the only argv-delivered path in the registry: Cursor and custom commands that explicitly use {prompt} still put prompts in argv. They are out of scope here and should not be described as fixed by this change.
Non-Goals
- Do not change prompt wording, diff truncation, output cleanup, model discovery, or model selection.
- Do not change interactive OpenCode terminal launch behavior.
- Do not change GitHub/GitLab/provider review flows.
- Do not change Cursor argv delivery or custom command
{prompt}semantics. - Do not add a second OpenCode execution path.
Design
- Change the OpenCode preset spec to
promptDelivery: 'stdin'. - Add a short why-comment near that setting: OpenCode prompt input can contain large staged diffs and must stay out of argv.
- Remove
promptfrom OpenCodebuildArgs. The preset args must be exactlyrun --model <model> --agent build --format default, plus--variant <thinkingLevel>when present. - Keep
modelSource,modelDiscovery, default model, and dynamic-model fallback behavior unchanged. - Keep preset
agentCommandOverridebehavior unchanged: an override such asnpx opencodeprefixes the same OpenCode args, while the prompt remainsstdinPayload. - Accept the shared-spec blast radius: OpenCode prompts for commit-message, PR-field, and branch-name generation all move to stdin because they all call
planCommitMessageGeneration(...). If the requirement is commit-message-only, this design is wrong and needs operation-aware delivery instead.
Data Flow
- Source Control AI receives a staged context and builds the prompt. The context is not atomic: branch, staged summary, and staged patch are read by separate git commands before generation.
planCommitMessageGeneration(...)validates the agent, model, and thinking level against the existing dynamic/static model rules.- Because OpenCode becomes stdin-delivered, the planner passes an empty prompt into
buildArgsand stores the real prompt inplan.stdinPayload. - Local generation writes
stdinPayloadto the child process stdin. - SSH generation serializes the same plan to the relay as
{ stdin: plan.stdinPayload }, and the relay writes it to the remote child process stdin after spawn.
This fix removes the prompt from process argv only. It does not reduce prompt memory, model latency, token use, or diff truncation pressure. Remote execution still sends the prompt in one JSON-RPC frame; the relay frame cap is 16 MB, and today's prompt budget is under that, but future budget increases must account for it.
Edge Cases
- Large staged diff: the prompt must be absent from
args, including no empty-string placeholder. - Thinking level selected: preserve
--variant <level>. - No thinking level selected: omit
--variant. - Dynamic OpenCode model not present in the seed catalog: preserve the existing dynamic fallback and still pass the selected model id.
- Agent command override:
npx opencodebecomesbinary: 'npx',args: ['opencode', 'run', ...], with prompt instdinPayload. - Windows batch shims: removing the prompt from argv also removes prompt metacharacters from the batch command line; do not route the prompt through
{prompt}for this preset. - SSH: the prompt is JSON-serialized as
stdin, not shell-quoted, and is written by the relay after spawn. - SSH payload size: this is safe for the current prompt budget, not an unbounded transport. Anything near 16 MB needs streaming or another transport.
- OpenCode ignores stdin: the smoke test must catch this before shipping because
--helpdoes not document stdin. - Output cleanup remains unchanged; OpenCode stderr/status chatter should be handled by existing failure and cleanup logic.
Consistency And Concurrency
- No cache invalidation is introduced. Each generation plans from the request's current settings and the staged context supplied to that request.
- The staged context is not a true snapshot. If the index changes between the summary and patch reads, or after the context is read, the generated message can describe a mixed or stale view. That is existing behavior and not fixed here.
- Multiple windows can request generation for the same worktree. Local execution does not serialize same-
cwdrequests; the cancel map is keyed byoperation:local:<cwd>and the newest request overwrites the reachable cancel token. Do not claim this change fixes local multi-window races. - SSH execution queues matching
operation + cwdlanes inside aSshGitProviderinstance. The relay itself tracks only one active child per lane and cancels a prior active child if another request for the same lane reaches it, which matters if separate provider instances/windows bypass the same queue. - Settings or command overrides changed while a generation is in flight affect the next request only; the current child keeps its immutable plan.
Test Plan
src/shared/commit-message-agent-spec.test.ts: add OpenCode coverage assertingpromptDelivery === 'stdin', no prompt or empty placeholder inbuildArgs, variant preservation, and no variant when absent.src/shared/commit-message-plan.test.ts: update OpenCode expectations sostdinPayloadreceives the prompt and argv excludes it; include an OpenCode command-override case.- Because
planCommitMessageGeneration(...)is reused by commit-message, PR-field, and branch-name generation, the plan tests are the main regression coverage for the shared blast radius. - Keep existing Codex stdin, custom-command stdin, local child stdin, SSH provider, and relay stdin tests passing.
- Run targeted Vitest for the touched shared tests, plus
pnpm typecheckandpnpm lintfor the implementation branch. - Run the OpenCode stdin smoke test from the feasibility gate. If it cannot run, do not mark implementation risk as resolved.
Lightweight Eng Review
- Scope: shared preset-agent plumbing and shared tests, with an explicit shared-operation blast radius for OpenCode commit-message, PR-field, and branch-name prompts. No renderer redesign, settings migration, hosted review flow, or provider-specific review behavior.
- Architecture/data flow: use the existing
promptDeliverycontract; local and SSH execution already honorstdinPayload. - Failure modes covered: prompt re-enters argv,
--variantis dropped, unknown dynamic model ids regress, command overrides change stdin behavior, remote JSON-RPC frame limits are ignored, or OpenCode accepts the flags but ignores stdin. - Performance/blast radius: no renderer, watcher, IPC subscription, or startup cost. Local argv shrinks; remote payload size is unchanged because the prompt still crosses JSON-RPC as stdin data in one frame.
- UI quality bar: no visual change; skip visual-design review.
- Screenshots: none required for this headless invocation change. If PR evidence is needed, prefer unit-test output and the controlled OpenCode stdin smoke result.
- Residual risk: OpenCode stdin prompt input is not documented by
opencode run --help; remote hosts may have different OpenCode versions; existing local multi-window cancellation races and staged-context non-atomicity remain.
Rollout
- Update OpenCode spec delivery and args.
- Update shared spec and planner tests.
- Run targeted tests, typecheck, lint, and the OpenCode smoke test before shipping.