Skip to content

Give a harness its argv back - #1178

Merged
edwin-zvs merged 1 commit into
mainfrom
cli-flag-order
Aug 2, 2026
Merged

Give a harness its argv back#1178
edwin-zvs merged 1 commit into
mainfrom
cli-flag-order

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

The bug

Harness argv forwarding — construct new <harness> <args...> — is completely broken. Passing any harness argv kills the session with spawn adapter for <harness>, about five seconds in.

Every documented example fails on main:

invocation source main
construct new claude --model opus docs/harnesses.md:51
construct new --prompt "fix tests" claude --permission-mode acceptEdits docs/harnesses.md:50
construct new --title "server logs" shell -lc 'tail -f server.log' spec 0120's own example
construct new shell (no argv)

This violates spec 0120, which is accepted and 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:

let combined_args = { let mut a = adapter_cfg.args.clone(); a.extend(params.args.clone()); a };

Every built-in adapter is dispatched as construct __adapter <name>, which accepts no arguments:

$ construct __adapter shell -lc 'echo hi'
error: unexpected argument '-l' found
Usage: construct __adapter shell [OPTIONS]

So the adapter process died at startup, never bound its socket, and the daemon's 5s connect_with_retry expired — 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()) from start_params), so the command-line copy was pure corruption. config.toml already 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-tui failed. Construct's own flags go before the harness (spec 0120's positional grammar), so --no-tui landed in harness argv — and hit this.

Also fixed

  • scripts/smoke.sh — passed --cwd after 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 fixed sleep 0.4 lost the startup race, failing at the first ping. Now runs green end to end, in the right sandbox.
  • .claude/skills/verify/SKILL.md — documented construct 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:

test a_session_with_harness_argv_starts ... ok
test harness_argv_is_what_the_harness_runs ... ok
test a_prompt_still_runs_without_argv ... ok
test result: ok. 3 passed; 0 failed

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.sh exits OK with 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_params would stop seeing them there. Every built-in adapter reads params.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-tui still doesn't suppress the TUI: --no-tui goes 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) and crates/e2e (tests) — the daemon is embedded in the single binary:

/Users/moon/construct/.claude/worktrees/cli-flag-order/target/debug/construct

`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.
@edwin-zvs
edwin-zvs merged commit 3ffbd1a into main Aug 2, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the cli-flag-order branch August 2, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant