fix: deliver harness prompts off argv
Summary
- keep one-shot persona tasks out of argv for Claude, Codex, OpenCode, and Grok
- deliver Claude/Codex/OpenCode prompts through stdin and Grok prompts through a private temporary
--prompt-file - share the process launcher between cloud runtime and the CLI persona improver, including timeout and loud stdin/EPIPE handling
- cover the off-argv invariant, a byte-for-byte 200 KB stdin round trip, and Grok prompt-file cleanup on child failure
Breaking API change
buildNonInteractiveSpec() no longer includes the task in NonInteractiveSpec.args; it returns a required prompt delivery descriptor instead. External consumers that previously spawned only spec.bin and spec.args must now deliver spec.prompt through stdin or a prompt file as directed. Ignoring the new field produces a prompt-less command. This is also called out under [Unreleased] in the persona-kit changelog.
Why
Linux applies two different exec limits: ARG_MAX caps argv + envp in aggregate, while MAX_ARG_STRLEN caps each individual argv element at 32 pages (131,072 bytes on the production sandbox). The prior implementation placed the complete per-run task in one argv element, so a daily-ship prompt containing PR facts could fail with spawn E2BIG even while the aggregate argv + env block remained below ARG_MAX.
On the probe host, getconf ARG_MAX was 1,048,576 bytes and the representative environment was about 7.1 KB, so prompt size dominated. macOS does not reproduce Linux's 128 KiB single-element boundary; this PR therefore tests the portable invariant directly: the 200 KB task marker never appears in argv and the remaining per-argument and total argv byte budgets stay below 128 KiB.
Empirical CLI probe
These canaries were run on macOS with the locally installed versions below. Each prompt asked the model to return a unique MAGIC_TOKEN; success means the token appeared in model stdout, not merely that the command exited successfully.
| Harness | Host version | Off-argv delivery | Canary result |
|---|---|---|---|
| Claude | 2.1.211 | bare stdin with --print --output-format text | passed |
| Codex | 0.144.0 | stdin with codex exec ... - | passed |
| OpenCode | 1.17.18 | bare stdin with opencode run --format default | passed |
| Grok | 0.2.99 | real temp file via --prompt-file <path> | passed |
Negative evidence: OpenCode -f/--file is an attachment mechanism, not a prompt replacement (You must provide a message or a command without a positional message). Grok bare stdin failed with Device not configured, so it uses a real temporary prompt file which is deleted in finally.
Scope of evidence: the real CLI canaries above used small prompts on macOS and the host versions shown. The automated 200 KB tests use trivial node -e children and prove only that our launcher delivers bytes intact, converts an early stdin close to a loud failure, and cleans up files. They do not prove that the production versions of Claude, Codex, or OpenCode consume a 200 KB stdin prompt without early-closing inside the Linux sandbox. That far-side E2E remains for the post-deploy real trigger.
Production/version evidence
agentworkforce deployments logs daily-ship --tail 200 --json showed the large-prompt spawn E2BIG. Inspection of the affected sandbox showed @agentworkforce/runtime and persona-kit 4.1.17 on disk even though snapshot metadata said runtime-4.1.24; the cloud tick was reinstalling the pinned 4.1.17 package. That stale pin also explains the separate OpenCode help dump (the OpenCode flag fix shipped in 4.1.20). This PR addresses the structural argv delivery bug across all four harnesses; deployment/version correction is tracked separately.
Validation
- persona-kit: 290/290 passing
- focused runtime suites: 21/21 passing (process helper, cloud defaults, runner)
- runner suite independently: 12/12 passing; exact relocated prompt content is asserted for Claude, Codex, and Grok
- CLI typecheck and build passing
- full runtime comparison with the exact same Node 22 binary:
- clean
origin/main: 117/130 passing, 13 failing - this branch: 120/133 passing, 13 failing
- the same 13 patched-Node local-preview gate tests fail in both; the three added process tests account for the delta
- clean
No npm publish is included.