Give a harness its argv back - #1178
Merged
Merged
Conversation
`construct new <harness> <args...>` is supposed to forward every token after the harness verbatim to that harness's CLI — spec 0120, accepted, with `construct new --title "server logs" shell -lc 'tail -f server.log'` as its own worked example. None of it works. Passing any harness argv at all kills the session: construct new claude --model opus (docs/harnesses.md) construct new --prompt "fix tests" codex --approval-mode never construct new shell -lc 'echo hi' (spec 0120) all fail with `spawn adapter for <harness>` about five seconds in. The daemon appended the session's args to the *adapter process's* dispatch command line as well as sending them in the start params. Every built-in adapter is dispatched as `construct __adapter <name>`, which takes no arguments: $ construct __adapter shell -lc 'echo hi' error: unexpected argument '-l' found So the adapter died on startup, never bound its socket, and the daemon's 5s connect retry expired — surfacing as a spawn failure that named neither argv nor the flag that caused it. The args were already reaching the adapter correctly over the socket, so the extra copy on the command line was pure corruption. Config calls that field "dispatch args"; now it only carries those, at create and at respawn. Also fixed, all found by following the same thread: - scripts/smoke.sh passed `--cwd` *after* the harness, so it went to the shell and the session was created in the wrong directory; it had no `--no-tui`, so it could never capture the id it assigns; and a fixed `sleep 0.4` lost the startup race and failed at the first `ping`. The script now runs green end to end. - The verify skill documented `construct new shell "" --no-tui`, which puts `--no-tui` in harness argv — the invocation that led here. Spec 0120 gains the invariant that was violated: forwarded argv belongs to the session and travels in its start params; an adapter's own command line is dispatch only. Covered by crates/e2e/tests/harness_argv.rs: a session with argv starts, the argv is what the harness actually ran (asserted against the PTY log), and the prompt path still works.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Harness argv forwarding —
construct new <harness> <args...>— is completely broken. Passing any harness argv kills the session withspawn adapter for <harness>, about five seconds in.Every documented example fails on main:
construct new claude --model opusdocs/harnesses.md:51construct new --prompt "fix tests" claude --permission-mode acceptEditsdocs/harnesses.md:50construct new --title "server logs" shell -lc 'tail -f server.log'construct new shell(no argv)This violates spec 0120, which is
acceptedand mandates exactly this forwarding.Root cause
The daemon appended the session's args to the adapter process's dispatch command line, in addition to sending them in the start params:
Every built-in adapter is dispatched as
construct __adapter <name>, which accepts no arguments:So the adapter process died at startup, never bound its socket, and the daemon's 5s
connect_with_retryexpired — surfacing as a spawn failure that named neither argv nor the flag responsible.The args were already reaching the adapter correctly over the socket (every adapter does
args.extend(params.args.clone())fromstart_params), so the command-line copy was pure corruption.config.tomlalready calls that field "dispatch args"; now it only carries those — at create and at respawn, which had the same bug.How it was found
Chasing why
construct new shell --no-tuifailed. Construct's own flags go before the harness (spec 0120's positional grammar), so--no-tuilanded in harness argv — and hit this.Also fixed
scripts/smoke.sh— passed--cwdafter the harness, so it went to the shell and the session was created in the wrong directory; had no--no-tui, so it could never capture the id it assigns to$SID; and a fixedsleep 0.4lost the startup race, failing at the firstping. Now runs green end to end, in the right sandbox..claude/skills/verify/SKILL.md— documentedconstruct new shell "" --no-tui, the broken form that led here. Corrected, with the ordering rule spelled out.specs/0120— records the invariant that was violated: forwarded argv belongs to the session and travels in its start params; an adapter's own command line is dispatch only.Verification
New
crates/e2e/tests/harness_argv.rs, all passing:The middle one asserts the argv is what the harness actually ran, by matching a marker in the session's PTY log — not merely that create returned an id.
Manually against a fresh daemon, every previously-broken form now works, including survival across a daemon restart (exercising the respawn path), and
scripts/smoke.shexitsOKwith the session in/private/tmp/smoke-after.Note for review
This is a behavior change for custom/plugin adapters: an external adapter binary that relied on receiving session args on its command line rather than from
start_paramswould stop seeing them there. Every built-in adapter readsparams.args, and the config field is documented as dispatch args, so this looks like the intended contract — but worth a second opinion if any out-of-tree adapter depends on the old shape.construct new shell --no-tuistill doesn't suppress the TUI:--no-tuigoes to the shell, per spec 0120's deliberate grammar. It now fails as a shell argument error rather than an opaque spawn error. Changing that would mean changing the grammar, which this PR does not do.Binaries
Touches
crates/daemon(the fix) andcrates/e2e(tests) — the daemon is embedded in the single binary:/Users/moon/construct/.claude/worktrees/cli-flag-order/target/debug/construct