Skip to content

Bridge: deliver to Claude Code and Pi on Windows, and let them report back - #19

Closed
ahmad-ajmal wants to merge 4 commits into
mainfrom
ahmad/bridge-windows-spawn
Closed

ahmad-ajmal wants to merge 4 commits into
mainfrom
ahmad/bridge-windows-spawn

Conversation

@ahmad-ajmal

@ahmad-ajmal ahmad-ajmal commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Found while testing #18 end to end with a real agent. On Windows, agent-app <dir> bridge start could not complete a single task with Claude Code or Pi. Four problems, one commit each:

1. The harness never started: spawn EINVAL.

  • Cause: npm installs claude, codex and gemini behind a .cmd wrapper, and Node refuses to spawn a batch file without a shell (CVE-2024-27980).
  • Made worse: the error was thrown synchronously, outside the 'error' handler. The task sat claimed with nobody running it until the app redelivered it to exhaustion (~5 min, then redelivery_exhausted).
  • Fix: launchSpec() reads the npm shim and spawns what it starts (node <cli.js> or the native .exe), with the prompt still an argument array. The "harness is not a shell" contract holds.
  • Other batch files: any other .cmd/.bat is reported unavailable on the ladder and refused before anything is claimed.
  • Throw guard: a synchronous spawn throw now fails the task at once as harness_spawn_failed.

2. The agent couldn't use the a2app CLI.

  • Cause: headless claude -p has nobody to approve a command, so every a2app . call came back "This command requires approval".
  • Observed: the agent did the triage, then read .agent-token, tried curl, and started editing the adapter's state file by hand.
  • Fix: the built-in profile now grants exactly Bash(a2app:*) and denies Edit/Write/NotebookEdit. A delivered task is done through the app's API, not its files.

3. The heartbeat overwrote the agent's own step.

  • Cause: every 20 s it sent step: "running in claude", so the app flipped between the agent's real step and that filler.
  • Fix: it now sends no step, and every adapter keeps the last one.

4. The Pi plugin route was refused.

  • Cause: the Pi plugin registers pi -p {prompt}, and on Windows pi is pi.cmd. That file is not npm-generated: it is the single line node "%~dp0pi-launcher.js" %*.
  • Fix: that shape is now recognised, strictly. Apart from @echo off, blank lines, comments and setlocal/endlocal, it must be the whole file. A wrapper that does anything more is still refused.

Verified

  • New tests in framework/cli/test/bridge.test.mjs:
    • shim parsing
    • on Windows, delivery through an npm-style shim with a payload of cmd.exe metacharacters (arrives verbatim, nothing executed), and a non-shim .bat refused before any claim
    • everywhere, a spawn that throws fails the task immediately, and heartbeats carry no step
  • Tests catch the bugs: run against main, the new tests fail exactly as the real bug did (tasks stuck working).
  • Real runs: with Enforce agent-state UX for app→agent features #18, Claude Code completed two real triage tasks through bridge start. It reported its own steps and completed with summaries, and the View showed them live.
  • Pi: delivered real tasks to Pi (0.99.2) through both the plugin route (pi -p {prompt} → pi.cmd) and a direct node pi-launcher.js route. Both completed with summaries, writing notes, a due date and a status change that the View showed live. The Pi plugin build, test and typecheck also pass.
  • Suite: CLI tests and conformance green.

🤖 Generated with Claude Code

ahmad-ajmal and others added 4 commits October 2, 2026 14:15
On Windows, `bridge start` could never deliver to Claude Code, Codex or
Gemini: npm installs them behind a .cmd wrapper, and Node refuses to spawn a
batch file without a shell (EINVAL, CVE-2024-27980). The throw happened
synchronously inside deliverHeadless, outside the 'error' handler, so the
poll loop logged "spawn EINVAL" and left the task claimed with nobody
running it until the app redelivered it to exhaustion (~5 minutes, then
`redelivery_exhausted`).

- launchSpec(): read an npm cmd-shim and spawn what it starts (node + the
  CLI script, or the native .exe) with the prompt still an argument array.
  Any other .cmd/.bat is refused with a reason; nothing is ever run through
  cmd.exe, so the "harness is not a shell" contract holds.
- The ladder reports a batch file it cannot start as unavailable, so
  `bridge start` refuses before claiming anything.
- A synchronous spawn() throw now fails the task at once as
  `harness_spawn_failed`.
- Tests: shim parsing; on Windows, delivery through an npm-style shim with a
  payload of cmd.exe metacharacters, and a non-shim .bat refused before any
  claim; everywhere, a spawn that throws fails the task immediately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The built-in Claude profile ran `claude -p <prompt>` with no permissions. In
headless mode nobody can approve a command, so every `a2app .` call the
prompt asks for came back "This command requires approval". Observed in a
real run: the agent did the work, then went looking for another way to
report it — reading `.agent-token`, trying curl, and finally editing the
adapter's state file by hand — while the task sat `working`.

Grant exactly the CLI the prompt names (`Bash(a2app:*)`) and deny file
edits: a delivered task is done through the app's API, never its files. The
variadic tool lists come before `-p` so they cannot swallow the prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The delivery heartbeat sent `step: "running in <harness>"` every 20 s, so an
agent that reported its own progress ("Writing triage notes…") had it
replaced within seconds, and the app's View flipped between the two for the
whole run. Seen in a real Claude Code run. The heartbeat now sends no step;
every adapter keeps the last one, so the agent's own words (or "delivered
to …" until it says anything) stay on screen.

BridgeContext takes an optional heartbeatMs (default HEARTBEAT_MS) so the
test can watch several beats in under a second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Pi plugin registers `pi -p {prompt}`, and on Windows `pi` resolves to
pi.cmd, which is not npm-generated: it is the single line
`node "%~dp0pi-launcher.js" %*`. The npm-shim parser did not know that
shape, so the bridge refused Pi outright.

Recognise it, strictly: after dropping `@echo off`, blank lines, comments
and setlocal/endlocal, that line must be the whole file, so running its
target directly skips nothing the batch file would have done. Anything
more is still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ahmad-ajmal ahmad-ajmal changed the title Bridge: deliver to Claude Code on Windows, and let it report back Bridge: deliver to Claude Code and Pi on Windows, and let them report back Oct 2, 2026
@ahmad-ajmal ahmad-ajmal closed this Oct 2, 2026
@ahmad-ajmal
ahmad-ajmal deleted the ahmad/bridge-windows-spawn branch October 2, 2026 13:52
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