@sagentlab/navarch-runtime 0.1.86 → 0.1.88

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.
package/README.md CHANGED
@@ -691,6 +691,14 @@ post-tool answer as the report, and reads aggregate `input_tokens` and
691
691
  `output_tokens` from the terminal `result` event. Gemini does not report USD
692
692
  cost, so an unknown value remains absent rather than becoming zero.
693
693
 
694
+ Gemini's native sandbox launcher converts piped stdin back into a `--prompt`
695
+ argument for its child. The adapter rejects prompts of 128 KiB or more when
696
+ that sandbox is enabled, with an explicit diagnostic; reducing context or
697
+ selecting another runtime preserves the worktree boundary. Navarch Docker
698
+ mode disables the nested Gemini sandbox by default and can still pipe larger
699
+ prompts. All Gemini runs reject prompts above 8 MiB because Gemini's stdin
700
+ reader silently truncates input beyond that limit. These checks use UTF-8 bytes.
701
+
694
702
  The OpenCode adapter uses `opencode run --format json` with the prompt on
695
703
  stdin, and normalizes `text` and `step_finish` JSONL events into
696
704
  the shared report/token/cost boundary. It translates the lease MCP document
@@ -722,6 +730,9 @@ rejects any single argv or environment string over 128 KiB
722
730
  (`MAX_ARG_STRLEN`), so an argv prompt made large review sessions fail with
723
731
  `spawn E2BIG` before the agent started. Docker-mode sessions forward the same
724
732
  stdin through `docker exec -i`.
733
+ Gemini's own sandbox relaunch is an exception to this transport guarantee,
734
+ as described above; moving the initial spawn to stdin does not remove its
735
+ internal argv limit.
725
736
 
726
737
  ## Architecture
727
738
 
@@ -740,7 +751,7 @@ cli.cts
740
751
  into Docker when NAVARCH_SANDBOX_MODE=docker
741
752
  4. selectAdapter(config.agentType) (adapters/index.cts) picks one AgentAdapter
742
753
  (adapters/types.cts) by NAVARCH_AGENT, then .run(...):
743
- - claudeCodeAdapter (adapters/claude.cts) — `claude -p --mcp-config <path> < prompt`
754
+ - claudeCodeAdapter (adapters/claude.cts) — `claude -p --mcp-config <path> --output-format stream-json --verbose < prompt`
744
755
  - codexAdapter (adapters/codex.cts) — `codex exec - --json -c mcp_servers.*=... < prompt`
745
756
  - geminiAdapter (adapters/gemini.cts) — `gemini --output-format stream-json < prompt`
746
757
  - openCodeAdapter (adapters/opencode.cts) — `opencode run --format json < prompt`
@@ -860,16 +871,17 @@ Claude Code headless → PR opens on GitHub → complete lands with evidence;
860
871
  secrets absent from disk after exit"):
861
872
 
862
873
  - Actually running `claude -p` headless and confirming its real
863
- `--output-format json` shape (WP-13). `adapters/claude.cts` now appends
864
- `--output-format json` by default and best-effort parses stdout as a single
865
- JSON result object shaped like `{ type, subtype, is_error, result,
874
+ result shape (WP-13). `adapters/claude.cts` appends
875
+ `--output-format stream-json --verbose` by default, so the uploaded
876
+ transcript records every message and tool call, and best-effort parses the
877
+ last `type: "result"` record on stdout as a JSON result object shaped like `{ type, subtype, is_error, result,
866
878
  session_id, total_cost_usd, usage: { input_tokens, output_tokens,
867
879
  cache_creation_input_tokens, cache_read_input_tokens } }` (see
868
880
  `exit-conditions.cts#parseClaudeJsonResult` for the exact assumption and
869
881
  fallback behavior). Unit tests (`tests/adapter.test.cts`,
870
882
  `tests/exit-conditions.test.cts`) cover the parsing and arg-construction
871
- logic against fixed JSON strings; what's untested offline is whether a real
872
- `claude -p --output-format json` invocation actually produces this shape.
883
+ logic against fixed JSON strings recorded from Claude Code 2.1; a future
884
+ CLI release could still change this shape.
873
885
  If it doesn't, parsing degrades to `null` and `tokensIn`/`tokensOut`/
874
886
  `costUsd` fall back to 0 (session.cts's existing default) — never a thrown
875
887
  error — but cost/spend data silently goes missing until the shape is
@@ -13,7 +13,7 @@ function hasFlag(args, flag) {
13
13
  }
14
14
  /**
15
15
  * Headless Claude Code adapter (project-plan.md §3.9 / implementation-plan.md
16
- * WP-07): `claude -p --mcp-config platform-mcp.json --output-format json`
16
+ * WP-07): `claude -p --mcp-config platform-mcp.json --output-format stream-json --verbose`
17
17
  * with the context bundle on stdin (`claude -p` reads its prompt from stdin
18
18
  * when none is given; Linux rejects any single argv string over 128 KiB, so
19
19
  * an argv prompt failed `spawn E2BIG` before Claude started). Returns raw stdout/stderr/exit signals (plus
@@ -25,11 +25,10 @@ function hasFlag(args, flag) {
25
25
  * (adapters/types.cts) — see adapters/codex.cts for the Codex CLI sibling
26
26
  * session.cts picks between via NAVARCH_AGENT (config.cts's `agentType`).
27
27
  *
28
- * NEEDS LIVE VERIFICATION (WP-13): `--output-format json` is assumed to make
29
- * `claude -p` print a single JSON result object with a `usage` block, per
30
- * Claude Code's documented headless-mode output. This cannot be confirmed
31
- * without a real `claude` binary in this offline build environment. If the
32
- * real shape differs, parseClaudeJsonResult() returns null and
28
+ * `--output-format stream-json` prints one JSON record per line and ends with
29
+ * the same result object (`type: "result"`, `usage`, `total_cost_usd`) that
30
+ * `--output-format json` prints alone; parseClaudeJsonResult() reads that
31
+ * last record. If the real shape differs, parseClaudeJsonResult() returns null and
33
32
  * tokensIn/tokensOut/costUsd fall back to 0 (session.cts's existing
34
33
  * default) rather than throwing -- see runtime/README.md.
35
34
  */
@@ -135,9 +134,14 @@ async function runClaudeAttempt(options, automaticFallback) {
135
134
  // Only append the default when the caller hasn't already asked for a
136
135
  // specific --output-format (extraArgs wins so an operator can opt back
137
136
  // into plain-text output, e.g. while diagnosing a live-verification
138
- // mismatch, without an adapter code change).
139
- if (!options.extraArgs.includes("--output-format")) {
140
- args.push("--output-format", "json");
137
+ // mismatch, without an adapter code change). stream-json records every
138
+ // message and tool call, which the task transcript viewer renders; plain
139
+ // `json` printed only the final result. Claude Code requires --verbose for
140
+ // stream-json under --print, and its last record is the same result object.
141
+ if (!hasFlag(options.extraArgs, "--output-format")) {
142
+ args.push("--output-format", "stream-json");
143
+ if (!hasFlag(options.extraArgs, "--verbose"))
144
+ args.push("--verbose");
141
145
  }
142
146
  args.push(...options.extraArgs);
143
147
  // Project/task policy is appended after machine-wide extra arguments so a
@@ -157,7 +161,7 @@ async function runClaudeAttempt(options, automaticFallback) {
157
161
  const raw = runOptions.dockerExec ? await runViaDocker(runOptions, args) : await runOnHost(runOptions, args);
158
162
  return attachUsage(raw);
159
163
  }
160
- /** Parses stdout for `claude -p --output-format json` usage and folds it onto the raw result (best-effort; leaves tokensIn/tokensOut/costUsd unset when parsing fails). */
164
+ /** Parses the final `claude -p --output-format stream-json` result record for usage and folds it onto the raw result (best-effort; leaves tokensIn/tokensOut/costUsd unset when parsing fails). */
161
165
  function attachUsage(result) {
162
166
  const parsed = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout);
163
167
  if (!parsed)
@@ -16,10 +16,20 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
16
16
  * temporary system-settings file and restricts the run to those server names.
17
17
  *
18
18
  * The prompt arrives on stdin rather than `--prompt`: Gemini runs headlessly
19
- * whenever stdin is not a TTY and uses the piped text as the prompt, and
20
- * Linux rejects any single argv string over 128 KiB (`spawn E2BIG`).
19
+ * whenever stdin is not a TTY and uses the piped text as the prompt. Its
20
+ * native sandbox puts stdin back into argv, so that path still has Linux's
21
+ * 128 KiB per-string limit. Gemini also caps stdin at 8 MiB.
21
22
  */
22
23
  async function runGeminiAdapter(options) {
24
+ if (options.signal?.aborted) {
25
+ return { exitCode: null, timedOut: false, killedByLeaseLoss: true, stdout: "", stderr: "" };
26
+ }
27
+ const promptBytes = Buffer.byteLength(options.prompt, "utf8");
28
+ // Gemini's readStdin() silently truncates at 8 MiB. Reject before any
29
+ // settings writes or process launch rather than execute a partial task.
30
+ if (promptBytes > 8 * 1024 * 1024) {
31
+ return inputFailure(`Gemini CLI stdin is limited to 8 MiB; the task prompt is ${promptBytes} bytes. Reduce the context bundle or select another runtime.`);
32
+ }
23
33
  const args = [];
24
34
  const env = { ...options.env };
25
35
  if (!hasArg(options.extraArgs, "--output-format", "-o")) {
@@ -57,12 +67,41 @@ async function runGeminiAdapter(options) {
57
67
  args.push(...options.extraArgs);
58
68
  if (options.model)
59
69
  args.push("--model", options.model);
70
+ // Gemini's sandbox launcher reads stdin and injects it back into --prompt
71
+ // for its child process (packages/cli/src/gemini.tsx injectStdinIntoArgs).
72
+ // Moving our first spawn's prompt to stdin cannot fix that second spawn.
73
+ // Check the effective flag, including operator overrides, without disabling
74
+ // the worktree guard. Even on macOS a Docker sandbox may launch on Linux.
75
+ if (geminiSandboxEnabled(args) && promptBytes >= 128 * 1024) {
76
+ return inputFailure(`Gemini CLI re-encodes stdin as --prompt when launching its native sandbox; the ${promptBytes}-byte task prompt exceeds Linux's argv limit. Reduce the context bundle or select another runtime that supports large prompts in its sandbox.`);
77
+ }
60
78
  const runOptions = { ...options, env };
61
79
  const raw = runOptions.dockerExec
62
80
  ? await runViaDocker(runOptions, args)
63
81
  : await runOnHost(runOptions, args);
64
82
  return attachGeminiOutput(raw);
65
83
  }
84
+ function inputFailure(stderr) {
85
+ return { exitCode: 1, timedOut: false, killedByLeaseLoss: false, stdout: "", stderr };
86
+ }
87
+ function geminiSandboxEnabled(args) {
88
+ // There is always a generated --sandbox flag unless extraArgs supplies one.
89
+ // Like yargs, later explicit values override earlier ones.
90
+ let enabled = false;
91
+ for (let index = 0; index < args.length; index++) {
92
+ const arg = args[index];
93
+ if (arg === "--sandbox" || arg === "-s") {
94
+ enabled = args[index + 1] !== "false";
95
+ }
96
+ else if (arg.startsWith("--sandbox=") || arg.startsWith("-s=")) {
97
+ enabled = arg.slice(arg.indexOf("=") + 1) !== "false";
98
+ }
99
+ else if (arg === "--no-sandbox") {
100
+ enabled = false;
101
+ }
102
+ }
103
+ return enabled;
104
+ }
66
105
  function hasArg(args, ...flags) {
67
106
  return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
68
107
  }
@@ -24,8 +24,9 @@ function agentAuthFailureReport(detail) {
24
24
  "Log the coding agent in again on this machine (`claude /login` or `codex login`), then resume the agent.";
25
25
  }
26
26
  /**
27
- * Best-effort parse of `claude -p --output-format json` stdout into the
28
- * assumed result shape above. Tries the whole trimmed stdout first (the
27
+ * Best-effort parse of `claude -p --output-format json` (or `stream-json`,
28
+ * whose last result record has the same shape) stdout into the assumed
29
+ * result shape above. Tries the whole trimmed stdout first (the
29
30
  * expected case); if that fails, tries each non-empty line that looks like a
30
31
  * JSON object, in case a warning/log line preceded the result (last such
31
32
  * line wins, since the final result is expected last). Returns null rather
@@ -39,6 +40,9 @@ function parseClaudeJsonResult(stdout) {
39
40
  const isUsableResult = (value) => typeof value === "object" &&
40
41
  value !== null &&
41
42
  !Array.isArray(value) &&
43
+ // stream-json output interleaves typed message/tool records with the
44
+ // final `type: "result"` record; only that record carries the outcome.
45
+ (!("type" in value) || value.type === "result") &&
42
46
  (!("result" in value) || typeof value.result === "string") &&
43
47
  ("result" in value || "usage" in value || "cost_usd" in value || "total_cost_usd" in value);
44
48
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.86",
3
+ "version": "0.1.88",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",