@cat-factory/executor-harness 1.50.2 → 1.50.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,10 +34,11 @@ function attributeCumulativeUsage(calls, usage) {
34
34
  * never argv), `onActivity` on every chunk, abort kills the child, and the close
35
35
  * handler resolves/rejects. The caller's `onEvent` accumulates the outcome.
36
36
  *
37
- * `prompt` is fed over stdin: for Claude Code that is just the task prompt (the
38
- * system prompt rides `--append-system-prompt`); for Codex which has no
39
- * system-prompt flag the caller prepends the composed system prompt to it so
40
- * the role + best-practice context is not lost.
37
+ * `prompt` is fed over stdin: for Claude Code that is normally just the task prompt (the
38
+ * system prompt rides `--append-system-prompt`), unless the system prompt is too large for
39
+ * argv, in which case it is folded into `prompt` (see `carryClaudeSystemPrompt`); for Codex
40
+ * which has no system-prompt flag the caller always prepends the composed system prompt
41
+ * so the role + best-practice context is not lost.
41
42
  */
42
43
  function streamCli(cli, prompt, opts, env, secrets, onEvent) {
43
44
  const { command, args } = cli;
@@ -122,6 +123,40 @@ function streamCli(cli, prompt, opts, env, secrets, onEvent) {
122
123
  });
123
124
  });
124
125
  }
126
+ /**
127
+ * Fold a composed system prompt into the task prompt so the role + best-practice context
128
+ * rides stdin as a single user turn. Used by the Codex runner (no system-prompt flag) and
129
+ * by the Claude runner's argv-overflow fallback. Empty system prompt ⇒ the task prompt is
130
+ * returned unchanged.
131
+ */
132
+ function foldSystemPrompt(systemPrompt, userPrompt) {
133
+ return systemPrompt ? `${systemPrompt}\n\n---\n\n${userPrompt}` : userPrompt;
134
+ }
135
+ /**
136
+ * Linux caps a SINGLE argv string at MAX_ARG_STRLEN (32 pages = 128 KiB) — a per-string limit,
137
+ * distinct from (and reached long before) the far larger total ARG_MAX for argv + env combined. A
138
+ * system prompt with best-practice fragments folded in can exceed that per-string cap, and `execve`
139
+ * then fails the whole spawn with `E2BIG` before the agent runs at all — the failure mode seen on
140
+ * the `pr-reviewer` step (a ~150 KiB composed prompt). The binding constraint is that per-string
141
+ * cap; 96 KiB stays comfortably under 128 KiB so the system-prompt argv can never approach it.
142
+ */
143
+ const MAX_ARGV_STRING_BYTES = 96 * 1024;
144
+ /**
145
+ * Decide how the Claude Code runner carries the composed system prompt. Small prompts ride
146
+ * `--append-system-prompt` (a real system turn, cacheable) as before; a prompt too large for a
147
+ * single argv string is instead folded into the stdin task prompt (like the Codex runner), which
148
+ * has no size ceiling. Pure so the branch is unit-testable without spawning the CLI.
149
+ */
150
+ export function carryClaudeSystemPrompt(systemPrompt, userPrompt) {
151
+ if (Buffer.byteLength(systemPrompt, 'utf8') <= MAX_ARGV_STRING_BYTES) {
152
+ return {
153
+ appendArgs: ['--append-system-prompt', systemPrompt],
154
+ prompt: userPrompt,
155
+ folded: false,
156
+ };
157
+ }
158
+ return { appendArgs: [], prompt: foldSystemPrompt(systemPrompt, userPrompt), folded: true };
159
+ }
125
160
  // ---------------------------------------------------------------------------
126
161
  // Claude Code
127
162
  // ---------------------------------------------------------------------------
@@ -161,18 +196,32 @@ export async function runClaudeCode(opts) {
161
196
  const stats = { toolCalls: 0, assistantChars: 0 };
162
197
  let summary = '';
163
198
  let usage;
199
+ // Decide how the composed system prompt is carried up front, so the telemetry seed below
200
+ // reflects what actually reaches the model: a small prompt rides `--append-system-prompt`
201
+ // (a real system turn), while an argv-overflowing prompt is folded into the first user turn
202
+ // — in which case NO system turn of ours is sent (the `E2BIG` fallback).
203
+ const { appendArgs, prompt, folded } = carryClaudeSystemPrompt(opts.systemPrompt, opts.userPrompt);
204
+ if (folded) {
205
+ opts.log?.warn('system prompt exceeds argv limit; folding into the task prompt', {
206
+ bytes: Buffer.byteLength(opts.systemPrompt, 'utf8'),
207
+ });
208
+ }
164
209
  // Reconstruct the full per-call request/response bodies for telemetry from the
165
210
  // stream. `--output-format stream-json --verbose` emits each turn as a near-verbatim
166
211
  // Anthropic Messages envelope, so `assistant` events carry the complete response
167
212
  // (text + tool_use blocks + usage), and `user` events carry the tool_result blocks
168
- // fed back — together the growing prompt transcript. We seed it with the two inputs
169
- // the harness supplies (they never appear in the stream): the system + first user
170
- // message. Bodies are credential-scrubbed (they can echo the leased token).
213
+ // fed back — together the growing prompt transcript. We seed it with the inputs the
214
+ // harness supplies (they never appear in the stream): the system + first user message
215
+ // when the prompt rides argv, or a single folded user turn when it doesn't — so the
216
+ // reconstruction never shows a system turn that was never sent. Bodies are
217
+ // credential-scrubbed (they can echo the leased token).
171
218
  const secrets = opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [];
172
- const messages = [
173
- { role: 'system', content: opts.systemPrompt },
174
- { role: 'user', content: opts.userPrompt },
175
- ];
219
+ const messages = folded
220
+ ? [{ role: 'user', content: prompt }]
221
+ : [
222
+ { role: 'system', content: opts.systemPrompt },
223
+ { role: 'user', content: opts.userPrompt },
224
+ ];
176
225
  const calls = [];
177
226
  const onEvent = (event) => {
178
227
  const type = event.type;
@@ -287,10 +336,9 @@ export async function runClaudeCode(opts) {
287
336
  'bypassPermissions',
288
337
  '--model',
289
338
  opts.model,
290
- '--append-system-prompt',
291
- opts.systemPrompt,
339
+ ...appendArgs,
292
340
  ],
293
- }, opts.userPrompt, opts, env, opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [], onEvent);
341
+ }, prompt, opts, env, opts.subscriptionToken ? secretsToRedact(opts.subscriptionToken) : [], onEvent);
294
342
  attributeCumulativeUsage(calls, usage);
295
343
  return {
296
344
  summary,
@@ -422,10 +470,9 @@ export async function runCodex(opts) {
422
470
  await writeFile(join(codexHome, 'config.toml'), 'cli_auth_credentials_store = "file"\n', 'utf8');
423
471
  }
424
472
  // Codex has no system-prompt flag, so fold the composed role + best-practice
425
- // context into the prompt itself (Claude Code instead rides --append-system-prompt).
426
- const prompt = opts.systemPrompt
427
- ? `${opts.systemPrompt}\n\n---\n\n${opts.userPrompt}`
428
- : opts.userPrompt;
473
+ // context into the prompt itself (Claude Code instead rides --append-system-prompt,
474
+ // falling back to this same fold when the prompt overflows argv).
475
+ const prompt = foldSystemPrompt(opts.systemPrompt, opts.userPrompt);
429
476
  // Codex's `exec --json` is far thinner than Claude Code's stream: it surfaces only
430
477
  // flat assistant text and (on `token_count` events) the per-turn `last_token_usage`
431
478
  // plus a cumulative total. It never exposes the request transcript or structured