@kontextmind/kxm 0.6.0

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.
Files changed (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,2095 @@
1
+ #!/usr/bin/env node
2
+ // Dev-helper envelope over verified headless CLIs. Not a Phase 11 product adapter.
3
+ //
4
+ // Reads a `kxm.harness-request.v1` JSON envelope (file argument or stdin),
5
+ // dispatches it to an allowlisted provider CLI, and prints a
6
+ // `kxm.harness-result.v2` envelope on stdout. Transport `completed` requires
7
+ // an observed exit code of exactly 0 and observed stdout/stderr completion
8
+ // (close or both streams drained). Valid JSON alone is not complete output.
9
+ // Transport completion is not product `routing-record.v1` finalOutcome and is
10
+ // not a model claim.
11
+ // Raw model/terminal payloads stay in private 0600 sidecars, not metadata.
12
+
13
+ import { spawn, spawnSync } from "node:child_process";
14
+ import { createHash } from "node:crypto";
15
+ import {
16
+ chmodSync,
17
+ existsSync,
18
+ mkdirSync,
19
+ readFileSync,
20
+ realpathSync,
21
+ writeFileSync,
22
+ } from "node:fs";
23
+ import nodePath from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+
26
+ const { posix, win32, join } = nodePath;
27
+ const resolvePath = nodePath.resolve;
28
+
29
+ export const REQUEST_SCHEMA = "kxm.harness-request.v1";
30
+ export const RESULT_SCHEMA = "kxm.harness-result.v2";
31
+ export const OBSOLETE_RESULT_SCHEMA = "kxm.harness-result.v1";
32
+ export const ROUTING_INT_CAP = 1_000_000;
33
+ export const TOKEN_BASIS = "cumulative";
34
+ export const CONTEXT_OCCUPANCY_UNKNOWN = "unknown";
35
+ export const COST_BASIS = Object.freeze(["provider-reported", "list", "unmetered", "unknown"]);
36
+ export const MODEL_CLAIM_STATUSES = Object.freeze(["done", "partial", "fail", "blocked"]);
37
+ export const REVIEW_VERDICTS = Object.freeze(["PASS", "BLOCK"]);
38
+ export const CLAIM_SOURCES = Object.freeze(["structured_output", "result", "none"]);
39
+ export const CLAIM_COUNT_CAP = 1000;
40
+ export const TRANSPORT_STATUSES = Object.freeze(["completed", "failed", "interrupted"]);
41
+ export const TRANSPORT_STAGES = Object.freeze(["preflight", "auth", "spawn", "run"]);
42
+ export const STOP_REASONS = Object.freeze(["end_turn", "stop", "error", "aborted", "toolUse", "unrecognized"]);
43
+ export const ERROR_CODES = Object.freeze([
44
+ "preflight_failed",
45
+ "auth_failed",
46
+ "spawn_failed",
47
+ "empty_payload",
48
+ "timed_out",
49
+ "signaled",
50
+ "model_error",
51
+ "turn_failed",
52
+ "stop_error",
53
+ "stop_aborted",
54
+ "normalization_failed",
55
+ "write_failed",
56
+ "stdio_incomplete",
57
+ "unknown_exit",
58
+ "unrecognized",
59
+ ]);
60
+ const DEFAULT_KILL_GRACE_MS = 2_000;
61
+ const KNOWN_RESULT_SCHEMAS = Object.freeze([RESULT_SCHEMA, OBSOLETE_RESULT_SCHEMA]);
62
+
63
+ export const EFFORT = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
64
+
65
+ /** Pi must not impersonate a provider that has a native harness rule. */
66
+ export const NATIVE_PI_BRAKE_PROVIDERS = Object.freeze([
67
+ "anthropic",
68
+ "openai",
69
+ "xai",
70
+ "moonshot",
71
+ "google",
72
+ "deepseek",
73
+ ]);
74
+
75
+ /** Verified Pi aggregator prefixes. Not a catalog and not a writer grant. */
76
+ export const PI_ALLOWED_PROVIDERS = Object.freeze(["openrouter", "nous-portal"]);
77
+
78
+ /** Exact helper model string after `@jayteelabs/pi-nous-portal-provider` is installed. */
79
+ export const PI_NOUS_PORTAL_HY4 = "nous-portal/tencent/hy4-preview";
80
+
81
+ /** Narrowly admitted Pi writer. Other Pi models need reviewed route admission. */
82
+ export const PI_ADMITTED_WRITER = "openrouter/qwen/qwen3-coder-plus";
83
+
84
+ const REQUEST_KEYS = Object.freeze([
85
+ "schema",
86
+ "role",
87
+ "harness",
88
+ "model",
89
+ "effort",
90
+ "permission",
91
+ "prompt_file",
92
+ "cwd",
93
+ "skills",
94
+ "hooks",
95
+ "max_cost_usd",
96
+ "output_schema",
97
+ "timeout_ms",
98
+ "output_dir",
99
+ "max_turns",
100
+ ]);
101
+
102
+ const UNSUPPORTED_LAUNCHER = /\.(cmd|bat|ps1)$/i;
103
+
104
+ const AGY_MODELS = Object.freeze([
105
+ "gemini-3.8-flash-high",
106
+ "gemini-3.8-flash-medium",
107
+ "gemini-3.8-flash-low",
108
+ "gemini-3.7-flash-high",
109
+ "gemini-3.7-flash-medium",
110
+ "gemini-3.7-flash-low",
111
+ "gemini-3.6-flash-high",
112
+ "gemini-3.6-flash-medium",
113
+ "gemini-3.6-flash-low",
114
+ "gemini-3.1-pro-high",
115
+ "gemini-3.1-pro-low",
116
+ ]);
117
+
118
+ /** Tab-separated `id<TAB>label` rows from `agy models` (committed probe fixture). */
119
+ const AGY_MODEL_ROW = /^[a-z0-9][a-z0-9.+_-]*\t+\S/im;
120
+
121
+ function agyModelsListed(text) {
122
+ return AGY_MODEL_ROW.test(String(text ?? ""));
123
+ }
124
+
125
+ /** Verified helper routes only. Kimi/Gemini/DeepSeek CLIs fail closed. */
126
+ export const ROUTES = Object.freeze({
127
+ grok: {
128
+ provider: "xai",
129
+ roles: Object.freeze(["writer"]),
130
+ permissions: Object.freeze(["edit"]),
131
+ models: Object.freeze(["grok-4.6"]),
132
+ efforts: Object.freeze(["low", "medium", "high"]),
133
+ auth: Object.freeze({ args: ["models"], loginHint: "grok (OAuth to auth.x.ai)" }),
134
+ },
135
+ agy: {
136
+ provider: "google",
137
+ roles: Object.freeze(["writer", "experiment"]),
138
+ permissions: Object.freeze(["edit"]),
139
+ models: AGY_MODELS,
140
+ efforts: Object.freeze(["low", "medium", "high"]),
141
+ auth: Object.freeze({ args: ["models"], loginHint: "agy (Antigravity OAuth)" }),
142
+ },
143
+ claude: {
144
+ provider: "anthropic",
145
+ roles: Object.freeze(["planner", "reviewer-arch"]),
146
+ permissions: Object.freeze(["read-only"]),
147
+ models: Object.freeze(["fable"]),
148
+ efforts: Object.freeze(["low", "medium", "high", "xhigh", "max"]),
149
+ auth: Object.freeze({ args: ["auth", "status"], loginHint: "claude auth login" }),
150
+ },
151
+ codex: {
152
+ provider: "openai",
153
+ roles: Object.freeze(["reviewer-cli"]),
154
+ permissions: Object.freeze(["read-only"]),
155
+ models: Object.freeze(["gpt-5.6-sol"]),
156
+ efforts: Object.freeze(["low", "medium", "high"]),
157
+ auth: Object.freeze({ args: ["login", "status"], loginHint: "codex login" }),
158
+ },
159
+ pi: {
160
+ provider: "openrouter",
161
+ roles: Object.freeze(["experiment", "planner", "reviewer-arch", "reviewer-cli", "writer"]),
162
+ permissions: Object.freeze(["read-only", "edit"]),
163
+ models: Object.freeze([]),
164
+ efforts: EFFORT,
165
+ auth: Object.freeze({
166
+ args: ["auth", "check"],
167
+ loginHint: "pi /login openrouter or install @jayteelabs/pi-nous-portal-provider and /login Nous Research Portal",
168
+ }),
169
+ },
170
+ });
171
+
172
+ const UNVERIFIED_HARNESSES = Object.freeze(["kimi", "gemini", "deepseek"]);
173
+
174
+ export function clampEffort(harness, effort) {
175
+ const accepted = ROUTES[harness]?.efforts ?? [];
176
+ if (!effort) return { effort: undefined, clamped: undefined };
177
+ if (!EFFORT.includes(effort)) {
178
+ throw failClosed(`unknown effort ${effort}; expected one of ${EFFORT.join(", ")}`);
179
+ }
180
+ if (accepted.length === 0) return { effort: undefined, clamped: effort };
181
+ if (accepted.includes(effort)) return { effort, clamped: undefined };
182
+ const want = EFFORT.indexOf(effort);
183
+ let best = accepted[0];
184
+ for (const candidate of accepted) {
185
+ if (Math.abs(EFFORT.indexOf(candidate) - want) < Math.abs(EFFORT.indexOf(best) - want)) {
186
+ best = candidate;
187
+ }
188
+ }
189
+ return { effort: best, clamped: effort };
190
+ }
191
+
192
+ export function parseJson(text) {
193
+ if (typeof text !== "string") return undefined;
194
+ const trimmed = text.trim();
195
+ if (!trimmed) return undefined;
196
+ try {
197
+ return JSON.parse(trimmed);
198
+ } catch {
199
+ // Mixed stdout: take the outermost object if it parses.
200
+ }
201
+ const start = trimmed.indexOf("{");
202
+ const end = trimmed.lastIndexOf("}");
203
+ if (start < 0 || end <= start) return undefined;
204
+ try {
205
+ return JSON.parse(trimmed.slice(start, end + 1));
206
+ } catch {
207
+ return undefined;
208
+ }
209
+ }
210
+
211
+ function failClosed(message, stage = "preflight") {
212
+ const error = new Error(message);
213
+ error.failClosed = true;
214
+ error.stage = stage;
215
+ return error;
216
+ }
217
+
218
+ function piLoginHint(provider) {
219
+ if (provider === "nous-portal") {
220
+ return "pi install npm:@jayteelabs/pi-nous-portal-provider, then /login → subscription or API key → Nous Research Portal (or NOUS_API_KEY)";
221
+ }
222
+ if (provider === "openrouter") return "pi /login openrouter";
223
+ return ROUTES.pi.auth.loginHint;
224
+ }
225
+
226
+ function loginHint(harness, detail, provider) {
227
+ const hint = harness === "pi"
228
+ ? piLoginHint(provider)
229
+ : (ROUTES[harness]?.auth.loginHint ?? "the native harness CLI");
230
+ return `${detail} Log in with ${hint} and retry; native-provider Pi fallback is not used.`;
231
+ }
232
+
233
+ export function piProviderOf(model) {
234
+ if (typeof model !== "string" || !model.includes("/")) return undefined;
235
+ return model.slice(0, model.indexOf("/")).trim().toLowerCase();
236
+ }
237
+
238
+ export function piModelId(model) {
239
+ const provider = piProviderOf(model);
240
+ if (!provider) return undefined;
241
+ const id = model.slice(model.indexOf("/") + 1).trim();
242
+ return id || undefined;
243
+ }
244
+
245
+ function piProviderLabel(provider) {
246
+ if (provider === "nous-portal") return "Nous Portal";
247
+ if (provider === "openrouter") return "OpenRouter";
248
+ return "Pi provider";
249
+ }
250
+
251
+ export function piAuthCheckArgs(request) {
252
+ const provider = piProviderOf(request.model);
253
+ if (!provider) throw failClosed("pi model must be provider/id");
254
+ const args = [...ROUTES.pi.auth.args, "--provider", provider];
255
+ if (request.role === "writer") {
256
+ const modelId = piModelId(request.model);
257
+ if (!modelId) throw failClosed("pi writer requires provider/id");
258
+ args.push("--model", modelId, "--json");
259
+ }
260
+ return args;
261
+ }
262
+
263
+ function pathApi(platform) {
264
+ return platform === "win32" ? win32 : posix;
265
+ }
266
+
267
+ function isAbsoluteOn(path, platform) {
268
+ if (platform === "win32") return /^[a-zA-Z]:[\\/]/.test(path) || path.startsWith("\\\\");
269
+ return path.startsWith("/");
270
+ }
271
+
272
+ export function resolveLauncher(cliId, options = {}) {
273
+ const platform = options.platform ?? process.platform;
274
+ const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
275
+ const exists = options.existsSync ?? existsSync;
276
+ const delim = platform === "win32" ? ";" : ":";
277
+ const path = pathApi(platform);
278
+
279
+ if (!cliId || typeof cliId !== "string" || cliId !== cliId.trim()) {
280
+ throw failClosed("launcher id is required");
281
+ }
282
+ if (cliId.includes("\0")) throw failClosed("launcher id is invalid");
283
+
284
+ const explicit = cliId.includes("/") || cliId.includes("\\") || isAbsoluteOn(cliId, platform);
285
+ if (explicit) return assertWinExecutable(cliId, platform, options);
286
+
287
+ const dirs = pathEnv.split(delim).filter(Boolean);
288
+ let rejectedShim;
289
+ for (const dir of dirs) {
290
+ if (platform === "win32") {
291
+ const exeName = cliId.toLowerCase().endsWith(".exe") ? cliId : `${cliId}.exe`;
292
+ const exe = path.join(dir, exeName);
293
+ if (exists(exe)) return canonicalize(exe, { ...options, platform });
294
+ for (const ext of [".cmd", ".bat", ".ps1", ""]) {
295
+ const shim = path.join(dir, `${cliId}${ext}`);
296
+ if (exists(shim)) {
297
+ rejectedShim = shim;
298
+ break;
299
+ }
300
+ }
301
+ if (rejectedShim) break;
302
+ } else if (exists(path.join(dir, cliId))) {
303
+ return canonicalize(path.join(dir, cliId), { ...options, platform });
304
+ }
305
+ }
306
+ if (rejectedShim) {
307
+ throw failClosed(unsupportedLauncherMessage(rejectedShim, platform));
308
+ }
309
+ throw failClosed(`missing binary ${cliId}; install and authenticate the native CLI`);
310
+ }
311
+
312
+ function canonicalize(target, options = {}) {
313
+ const platform = options.platform ?? process.platform;
314
+ if (platform === "win32" && process.platform !== "win32") return target;
315
+ const realpath = options.realpathSync ?? realpathSync;
316
+ try {
317
+ return realpath(target);
318
+ } catch {
319
+ return resolvePath(target);
320
+ }
321
+ }
322
+
323
+ function assertWinExecutable(target, platform, options = {}) {
324
+ const base = pathApi(platform).basename(target);
325
+ if (UNSUPPORTED_LAUNCHER.test(base)) {
326
+ throw failClosed(unsupportedLauncherMessage(target, platform));
327
+ }
328
+ if (platform === "win32") {
329
+ if (!isAbsoluteOn(target, platform)) {
330
+ throw failClosed(unsupportedLauncherMessage(target, platform));
331
+ }
332
+ if (!base.toLowerCase().endsWith(".exe")) {
333
+ throw failClosed(unsupportedLauncherMessage(target, platform));
334
+ }
335
+ }
336
+ return canonicalize(target, { ...options, platform });
337
+ }
338
+
339
+ export function unsupportedLauncherMessage(target, platform = process.platform) {
340
+ return `unsupported-launcher: Windows helper dispatch is unverified and refuses ${pathApi(platform).basename(target)} `
341
+ + "(.cmd/.bat/.ps1/extensionless). Use an absolute .exe; do not run shims through a shell.";
342
+ }
343
+
344
+ export function parseAuth(harness, stdio, options = {}) {
345
+ const observedAt = options.observedAt ?? new Date().toISOString().slice(0, 10);
346
+ const stdout = stdio.stdout ?? "";
347
+ const stderr = stdio.stderr ?? "";
348
+ const code = stdio.exitCode ?? stdio.status ?? 0;
349
+ if (code !== 0) {
350
+ throw failClosed(loginHint(harness, `${harness} auth command exited ${code}.`), "auth");
351
+ }
352
+ if (harness === "grok") {
353
+ if (!/logged in/i.test(stdout)) {
354
+ throw failClosed(loginHint("grok", "grok models did not report a logged-in session."), "auth");
355
+ }
356
+ const method = /grok\.com/i.test(stdout) ? "grok.com" : "unknown";
357
+ if (method === "unknown") {
358
+ throw failClosed(loginHint("grok", "grok auth method could not be determined."), "auth");
359
+ }
360
+ return { loggedIn: true, method, observedAt };
361
+ }
362
+ if (harness === "claude") {
363
+ const payload = parseJson(stdout);
364
+ if (!payload || typeof payload !== "object") {
365
+ throw failClosed(loginHint("claude", "claude auth status was not parseable JSON."), "auth");
366
+ }
367
+ if (payload.loggedIn !== true) {
368
+ throw failClosed(loginHint("claude", "claude auth status is logged out."), "auth");
369
+ }
370
+ const method = typeof payload.authMethod === "string" ? payload.authMethod : undefined;
371
+ if (!method) {
372
+ throw failClosed(loginHint("claude", "claude auth method is undetermined."), "auth");
373
+ }
374
+ return { loggedIn: true, method, observedAt, subscriptionType: payload.subscriptionType };
375
+ }
376
+ if (harness === "codex") {
377
+ const text = `${stdout}\n${stderr}`;
378
+ if (!/logged in using chatgpt/i.test(text)) {
379
+ throw failClosed(loginHint("codex", "codex login status did not report ChatGPT auth."), "auth");
380
+ }
381
+ return { loggedIn: true, method: "ChatGPT", observedAt };
382
+ }
383
+ if (harness === "pi") {
384
+ const requested = typeof options.provider === "string" && options.provider.trim()
385
+ ? options.provider.trim().toLowerCase()
386
+ : undefined;
387
+ const text = `${stdout}\n${stderr}`;
388
+ const payload = parseJson(stdout);
389
+ const reportedFromJson = typeof payload?.provider === "string"
390
+ ? payload.provider.trim().toLowerCase()
391
+ : undefined;
392
+ const reportedFromText = /\bnous-portal\b/i.test(text)
393
+ ? "nous-portal"
394
+ : /\bopenrouter\b/i.test(text)
395
+ ? "openrouter"
396
+ : undefined;
397
+ const reported = reportedFromJson ?? reportedFromText ?? requested;
398
+ const jsonStatus = typeof payload?.status === "string" ? payload.status : undefined;
399
+ if (/not[_ ]ready/i.test(text) || (jsonStatus !== undefined && jsonStatus !== "ready")
400
+ || !/\bready\b/i.test(text)) {
401
+ throw failClosed(
402
+ loginHint("pi", `pi auth check did not report ${piProviderLabel(requested ?? reported)} ready.`, requested ?? reported),
403
+ "auth",
404
+ );
405
+ }
406
+ if (!reported || (requested && reported !== requested)) {
407
+ throw failClosed(loginHint("pi", "pi auth check provider could not be determined.", requested), "auth");
408
+ }
409
+ if (!PI_ALLOWED_PROVIDERS.includes(reported)) {
410
+ throw failClosed(
411
+ loginHint("pi", `pi helper allows only openrouter/* or nous-portal/*; ${reported} is not allowlisted.`, requested),
412
+ "auth",
413
+ );
414
+ }
415
+ return { loggedIn: true, method: reported, observedAt };
416
+ }
417
+ if (harness === "agy") {
418
+ const text = `${stdout}\n${stderr}`;
419
+ if (!agyModelsListed(text)) {
420
+ throw failClosed(loginHint("agy", "agy models did not print a models list."), "auth");
421
+ }
422
+ return { loggedIn: true, method: "antigravity-oauth", observedAt };
423
+ }
424
+ throw failClosed(`unknown harness ${harness} for auth parse`, "auth");
425
+ }
426
+
427
+ function claudeIsolationArgs(mcpConfigPath) {
428
+ return [
429
+ "--safe-mode",
430
+ "--strict-mcp-config",
431
+ "--mcp-config",
432
+ mcpConfigPath,
433
+ "--disable-slash-commands",
434
+ ];
435
+ }
436
+
437
+ function piDisableArgs(request) {
438
+ const args = [];
439
+ if (request.permission === "read-only") {
440
+ args.push("--no-extensions", "--no-skills", "--no-prompt-templates");
441
+ } else {
442
+ if (request.hooks === false) args.push("--no-extensions");
443
+ if (Array.isArray(request.skills) && request.skills.length === 0) args.push("--no-skills");
444
+ }
445
+ return args;
446
+ }
447
+
448
+ export function buildArgv(request, ctx = {}) {
449
+ const harness = request.harness;
450
+ const model = request.model;
451
+ const effort = request.effort;
452
+ const permission = request.permission;
453
+ const promptPath = ctx.promptPath ?? request.prompt_file;
454
+ const schemaPath = ctx.schemaPath ?? request.output_schema;
455
+ const schemaText = ctx.schemaText ?? (request.output_schema ? readFileSync(request.output_schema, "utf8") : undefined);
456
+ if (harness === "agy") {
457
+ const promptText = ctx.promptText ?? (promptPath ? readFileSync(promptPath, "utf8") : "");
458
+ return [
459
+ "-p",
460
+ promptText,
461
+ "--output-format",
462
+ "json",
463
+ "--dangerously-skip-permissions",
464
+ "--model",
465
+ model,
466
+ ...(effort ? ["--effort", effort] : []),
467
+ ...(schemaPath ? ["--json-schema", schemaPath] : []),
468
+ ...(request.timeout_ms !== undefined ? ["--print-timeout", `${request.timeout_ms}ms`] : []),
469
+ ];
470
+ }
471
+ if (harness === "grok") {
472
+ return [
473
+ "--prompt-file",
474
+ promptPath,
475
+ "-m",
476
+ model,
477
+ ...(effort ? ["--reasoning-effort", effort] : []),
478
+ "--always-approve",
479
+ "--no-subagents",
480
+ "--disable-web-search",
481
+ ...(request.max_turns !== undefined ? ["--max-turns", String(request.max_turns)] : []),
482
+ ...(schemaText !== undefined ? ["--json-schema", schemaText] : []),
483
+ "--output-format",
484
+ "json",
485
+ ];
486
+ }
487
+ if (harness === "claude") {
488
+ const mcpConfigPath = ctx.mcpConfigPath;
489
+ if (!mcpConfigPath) throw failClosed("claude read-only requires an empty MCP config sidecar");
490
+ return [
491
+ "-p",
492
+ "--model",
493
+ model,
494
+ ...(effort ? ["--effort", effort] : []),
495
+ "--tools",
496
+ "Read,Glob,Grep",
497
+ ...claudeIsolationArgs(mcpConfigPath),
498
+ ...(request.max_cost_usd !== undefined ? ["--max-budget-usd", String(request.max_cost_usd)] : []),
499
+ ...(schemaText !== undefined ? ["--json-schema", schemaText] : []),
500
+ "--output-format",
501
+ "json",
502
+ ];
503
+ }
504
+ if (harness === "codex") {
505
+ return [
506
+ "exec",
507
+ "-m",
508
+ model,
509
+ ...(effort ? ["-c", `model_reasoning_effort="${effort}"`] : []),
510
+ "--sandbox",
511
+ "read-only",
512
+ "--ignore-user-config",
513
+ ...(schemaPath ? ["--output-schema", schemaPath] : []),
514
+ "--json",
515
+ "-",
516
+ ];
517
+ }
518
+ if (harness === "pi") {
519
+ return [
520
+ "-p",
521
+ "--model",
522
+ model,
523
+ ...(effort ? ["--thinking", effort] : []),
524
+ ...(permission === "read-only" ? ["--tools", "read,grep,find,ls"] : ["-a"]),
525
+ ...piDisableArgs(request),
526
+ "--mode",
527
+ "json",
528
+ `@${promptPath}`,
529
+ ];
530
+ }
531
+ throw failClosed(`no argv builder for harness ${harness}`);
532
+ }
533
+
534
+ export function formatRunCost(result) {
535
+ const basis = result?.costBasis;
536
+ if (basis === "unmetered") return "unmetered";
537
+ if (basis === "unknown" || basis == null) return "unknown";
538
+ const billed = typeof result.costUsd === "number" && Number.isFinite(result.costUsd)
539
+ ? result.costUsd
540
+ : undefined;
541
+ const estimate = typeof result.providerReportedCostUsd === "number"
542
+ && Number.isFinite(result.providerReportedCostUsd)
543
+ ? result.providerReportedCostUsd
544
+ : undefined;
545
+ const amount = billed ?? estimate;
546
+ if (amount === undefined) return "unknown";
547
+ const dollars = `$${amount.toFixed(4)}`;
548
+ if (basis === "list") return `list ${dollars}`;
549
+ if (basis === "billed") return `billed ${dollars}`;
550
+ return `${basis} ${dollars}`;
551
+ }
552
+
553
+ function jsonlEvents(stdout) {
554
+ return String(stdout ?? "")
555
+ .split(/\r?\n/)
556
+ .map((line) => {
557
+ const trimmed = line.trim();
558
+ if (!trimmed) return undefined;
559
+ try {
560
+ return JSON.parse(trimmed);
561
+ } catch {
562
+ return undefined;
563
+ }
564
+ })
565
+ .filter(Boolean);
566
+ }
567
+
568
+ function addNumber(left, right) {
569
+ if (typeof right !== "number" || !Number.isFinite(right)) return left;
570
+ return (typeof left === "number" ? left : 0) + right;
571
+ }
572
+
573
+ function closeStopReason(value) {
574
+ if (value === undefined || value === null || value === "") return undefined;
575
+ if (typeof value === "string" && STOP_REASONS.includes(value)) return value;
576
+ return "unrecognized";
577
+ }
578
+
579
+ function joinDetail(...parts) {
580
+ return parts.filter((part) => typeof part === "string" && part.length > 0).join("\n");
581
+ }
582
+
583
+ export function normalizePi(stdout) {
584
+ const events = jsonlEvents(stdout);
585
+ if (events.length === 0) {
586
+ return {
587
+ emptyPayload: true,
588
+ errorCode: "empty_payload",
589
+ errorDetail: "empty JSON payload",
590
+ costBasis: "unknown",
591
+ };
592
+ }
593
+ const errorEvent = events.find((event) => event.type === "error" || event.type === "agent_error"
594
+ || event.stopReason === "error" || event.message?.stopReason === "error");
595
+ const assistantEnds = events.filter((event) => event.type === "message_end" && event.message?.role === "assistant");
596
+ const usageEvents = [];
597
+ const seen = new Set();
598
+ for (const event of assistantEnds) {
599
+ const identity = event.id ?? event.message?.id;
600
+ if (identity !== undefined && identity !== null && identity !== "") {
601
+ const key = String(identity);
602
+ if (seen.has(key)) continue;
603
+ seen.add(key);
604
+ }
605
+ const usage = event.message?.usage ?? {};
606
+ usageEvents.push({
607
+ model: event.message?.model,
608
+ tokensIn: usage.input,
609
+ tokensOut: usage.output,
610
+ cacheReadTokens: usage.cacheRead,
611
+ cacheCreationTokens: usage.cacheWrite,
612
+ costUsd: usage.cost?.total,
613
+ stopReason: closeStopReason(event.message?.stopReason),
614
+ });
615
+ }
616
+ const last = assistantEnds.at(-1)?.message;
617
+ let tokensIn;
618
+ let tokensOut;
619
+ let cacheReadTokens;
620
+ let cacheCreationTokens;
621
+ let costUsd;
622
+ for (const entry of usageEvents) {
623
+ tokensIn = addNumber(tokensIn, entry.tokensIn);
624
+ tokensOut = addNumber(tokensOut, entry.tokensOut);
625
+ cacheReadTokens = addNumber(cacheReadTokens, entry.cacheReadTokens);
626
+ cacheCreationTokens = addNumber(cacheCreationTokens, entry.cacheCreationTokens);
627
+ costUsd = addNumber(costUsd, entry.costUsd);
628
+ }
629
+ const rawStop = last?.stopReason;
630
+ const stopReason = closeStopReason(rawStop);
631
+ let errorCode;
632
+ let errorDetail;
633
+ if (errorEvent) {
634
+ errorCode = "model_error";
635
+ errorDetail = String(errorEvent.message?.errorMessage ?? errorEvent.error ?? errorEvent.message ?? "error");
636
+ } else if (stopReason === "error") {
637
+ errorCode = "stop_error";
638
+ errorDetail = "pi stopReason error";
639
+ } else if (stopReason === "aborted") {
640
+ errorCode = "stop_aborted";
641
+ errorDetail = "pi stopReason aborted";
642
+ }
643
+ if (rawStop !== undefined && stopReason === "unrecognized") {
644
+ errorDetail = joinDetail(errorDetail, `stopReason ${String(rawStop)}`);
645
+ }
646
+ return {
647
+ text: assistantText(last),
648
+ sessionId: events.find((event) => event.type === "session")?.id,
649
+ effectiveModel: last?.model,
650
+ tokensIn,
651
+ tokensOut,
652
+ cacheReadTokens,
653
+ cacheCreationTokens,
654
+ costUsd,
655
+ costBasis: costUsd === undefined ? "unknown" : "list",
656
+ stopReason,
657
+ usageEvents,
658
+ tokenBasis: TOKEN_BASIS,
659
+ ...(errorCode ? { errorCode } : {}),
660
+ ...(errorDetail ? { errorDetail } : {}),
661
+ };
662
+ }
663
+
664
+ function assistantText(message) {
665
+ if (!message) return "";
666
+ const content = message.content;
667
+ if (typeof content === "string") return content;
668
+ if (!Array.isArray(content)) return "";
669
+ return content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("");
670
+ }
671
+
672
+ export function normalizeCodex(stdout) {
673
+ const events = jsonlEvents(stdout);
674
+ if (events.length === 0) {
675
+ return {
676
+ emptyPayload: true,
677
+ errorCode: "empty_payload",
678
+ errorDetail: "empty JSON payload",
679
+ costBasis: "unknown",
680
+ };
681
+ }
682
+ const message = events.filter((event) => event.item?.type === "agent_message").pop();
683
+ const usage = events.find((event) => event.type === "turn.completed")?.usage ?? {};
684
+ const failure = events.find((event) => event.type === "turn.failed" || event.type === "error");
685
+ return {
686
+ text: message?.item?.text ?? "",
687
+ tokensIn: usage.input_tokens,
688
+ tokensOut: usage.output_tokens,
689
+ cacheReadTokens: usage.cached_input_tokens,
690
+ cacheCreationTokens: usage.cache_write_input_tokens,
691
+ reasoningTokens: usage.reasoning_output_tokens,
692
+ tokenBasis: TOKEN_BASIS,
693
+ costBasis: "unmetered",
694
+ ...(failure
695
+ ? {
696
+ errorCode: "turn_failed",
697
+ errorDetail: String(failure.message ?? failure.error?.message ?? "turn.failed"),
698
+ }
699
+ : {}),
700
+ };
701
+ }
702
+
703
+ export function resolveModelUsage(modelUsage, requestedModel) {
704
+ const entries = Object.entries(modelUsage ?? {});
705
+ const candidates = entries.map(([key]) => key);
706
+ if (entries.length === 0) {
707
+ return { effectiveModel: undefined, primary: undefined, auxiliary: [], candidates };
708
+ }
709
+ const exact = entries.find(([key]) => key === requestedModel);
710
+ if (exact) {
711
+ return {
712
+ effectiveModel: exact[0],
713
+ primary: exact[1],
714
+ auxiliary: entries.filter(([key]) => key !== exact[0]).map(([model, usage]) => usageRow(model, usage)),
715
+ candidates,
716
+ matched: "exact",
717
+ };
718
+ }
719
+ const needle = String(requestedModel ?? "").toLowerCase();
720
+ const unique = needle
721
+ ? entries.filter(([key]) => {
722
+ const lower = key.toLowerCase();
723
+ return lower === needle
724
+ || lower.startsWith(`${needle}-`)
725
+ || lower.split(/[^a-z0-9]+/).includes(needle);
726
+ })
727
+ : [];
728
+ if (unique.length === 1) {
729
+ const [model, usage] = unique[0];
730
+ return {
731
+ effectiveModel: model,
732
+ primary: usage,
733
+ auxiliary: entries.filter(([key]) => key !== model).map(([auxModel, auxUsage]) => usageRow(auxModel, auxUsage)),
734
+ candidates,
735
+ matched: "unique-segment",
736
+ };
737
+ }
738
+ return {
739
+ effectiveModel: "unknown",
740
+ primary: undefined,
741
+ auxiliary: entries.map(([model, usage]) => usageRow(model, usage)),
742
+ candidates,
743
+ matched: "none",
744
+ };
745
+ }
746
+
747
+ function usageRow(model, usage = {}) {
748
+ return {
749
+ model,
750
+ tokensIn: usage.inputTokens ?? usage.input_tokens,
751
+ tokensOut: usage.outputTokens ?? usage.output_tokens,
752
+ cacheReadTokens: usage.cacheReadInputTokens ?? usage.cache_read_input_tokens,
753
+ cacheCreationTokens: usage.cacheCreationInputTokens ?? usage.cache_creation_input_tokens,
754
+ costUsd: usage.costUSD ?? usage.costUsd,
755
+ costBasis: usage.costBasis,
756
+ };
757
+ }
758
+
759
+ export function normalizeAgy(payload) {
760
+ if (!payload || typeof payload !== "object") {
761
+ return {
762
+ emptyPayload: true,
763
+ errorCode: "empty_payload",
764
+ errorDetail: "empty JSON payload",
765
+ costBasis: "unmetered",
766
+ };
767
+ }
768
+ const usage = payload.usage && typeof payload.usage === "object" ? payload.usage : {};
769
+ const denied = Array.isArray(payload.denied_actions)
770
+ ? payload.denied_actions.filter((item) => item !== undefined && item !== null && String(item).length > 0)
771
+ : (typeof payload.denied_actions === "string" && payload.denied_actions.trim()
772
+ ? [payload.denied_actions.trim()]
773
+ : []);
774
+ const rawError = payload.error;
775
+ const errorText = rawError === undefined || rawError === null || rawError === ""
776
+ ? undefined
777
+ : String(rawError?.message ?? rawError);
778
+ const isTimeout = errorText === "timeout waiting for response";
779
+ let errorCode;
780
+ let errorDetail;
781
+ if (denied.length > 0) {
782
+ errorCode = "turn_failed";
783
+ errorDetail = joinDetail(`denied_actions: ${JSON.stringify(denied)}`, errorText);
784
+ } else if (payload.status === "SUCCESS") {
785
+ errorCode = undefined;
786
+ } else if (payload.status === "ERROR" && isTimeout) {
787
+ errorCode = "timed_out";
788
+ errorDetail = errorText;
789
+ } else if (payload.status === "ERROR") {
790
+ errorCode = "model_error";
791
+ errorDetail = errorText ?? "ERROR";
792
+ } else {
793
+ errorCode = "model_error";
794
+ errorDetail = errorText ?? `unrecognized agy status ${String(payload.status ?? "missing")}`;
795
+ }
796
+ const reportedModel = typeof payload.model === "string" && payload.model.trim()
797
+ ? payload.model.trim()
798
+ : undefined;
799
+ const text = typeof payload.response === "string" ? payload.response : "";
800
+ return {
801
+ text,
802
+ sessionId: typeof payload.conversation_id === "string" ? payload.conversation_id : undefined,
803
+ ...(reportedModel ? { effectiveModel: reportedModel } : {}),
804
+ tokensIn: usage.input_tokens,
805
+ tokensOut: usage.output_tokens,
806
+ cacheReadTokens: usage.cache_read_tokens,
807
+ reasoningTokens: usage.thinking_tokens,
808
+ totalTokens: usage.total_tokens,
809
+ tokenBasis: TOKEN_BASIS,
810
+ costBasis: "unmetered",
811
+ ...(payload.structured_output !== undefined ? { structuredOutput: payload.structured_output } : {}),
812
+ ...(errorCode ? { errorCode } : {}),
813
+ ...(errorDetail ? { errorDetail } : {}),
814
+ };
815
+ }
816
+
817
+ export function normalizeClaudeOrGrok(payload, requestedModel) {
818
+ if (!payload || typeof payload !== "object") {
819
+ return {
820
+ emptyPayload: true,
821
+ errorCode: "empty_payload",
822
+ errorDetail: "empty JSON payload",
823
+ costBasis: "unknown",
824
+ };
825
+ }
826
+ const usage = payload.usage ?? {};
827
+ const resolved = resolveModelUsage(payload.modelUsage ?? {}, requestedModel);
828
+ const detail = resolved.primary ?? {};
829
+ const tokensIn = detail.inputTokens ?? usage.input_tokens;
830
+ const tokensOut = detail.outputTokens ?? usage.output_tokens;
831
+ const cacheReadTokens = detail.cacheReadInputTokens ?? usage.cache_read_input_tokens;
832
+ const cacheCreationTokens = detail.cacheCreationInputTokens ?? usage.cache_creation_input_tokens;
833
+ const providerReportedCostUsd = payload.total_cost_usd ?? detail.costUSD;
834
+ const rawStop = payload.stop_reason ?? payload.stopReason;
835
+ const stopReason = closeStopReason(rawStop);
836
+ let errorCode;
837
+ let errorDetail;
838
+ if (payload.is_error) {
839
+ errorCode = "model_error";
840
+ errorDetail = String(payload.result ?? payload.error?.message ?? payload.error ?? "is_error");
841
+ } else if (stopReason === "error") {
842
+ errorCode = "stop_error";
843
+ errorDetail = "stopReason error";
844
+ } else if (stopReason === "aborted") {
845
+ errorCode = "stop_aborted";
846
+ errorDetail = "stopReason aborted";
847
+ } else if (payload.error) {
848
+ errorCode = "model_error";
849
+ errorDetail = String(payload.error?.message ?? payload.error);
850
+ }
851
+ if (rawStop !== undefined && stopReason === "unrecognized") {
852
+ errorDetail = joinDetail(errorDetail, `stopReason ${String(rawStop)}`);
853
+ }
854
+ let text = "";
855
+ let malformedText = false;
856
+ if (typeof payload.result === "string") {
857
+ text = payload.result;
858
+ } else if (payload.result !== undefined && payload.result !== null) {
859
+ malformedText = true;
860
+ } else if (typeof payload.text === "string") {
861
+ text = payload.text;
862
+ } else if (payload.text !== undefined) {
863
+ malformedText = true;
864
+ }
865
+ if (malformedText && !errorCode) {
866
+ errorCode = "normalization_failed";
867
+ errorDetail = joinDetail(errorDetail, "malformed optional text field");
868
+ }
869
+ return {
870
+ text,
871
+ sessionId: payload.sessionId ?? payload.session_id,
872
+ effectiveModel: resolved.effectiveModel,
873
+ tokensIn,
874
+ tokensOut,
875
+ cacheReadTokens,
876
+ cacheCreationTokens,
877
+ reasoningTokens: usage.reasoning_tokens ?? usage.output_tokens_details?.thinking_tokens,
878
+ providerReportedCostUsd,
879
+ costUsd: providerReportedCostUsd,
880
+ costBasis: resolveHelperCostBasis(detail.costBasis, providerReportedCostUsd),
881
+ stopReason,
882
+ tokenBasis: TOKEN_BASIS,
883
+ auxiliaryUsage: resolved.auxiliary,
884
+ candidateModels: resolved.candidates,
885
+ ...(errorCode ? { errorCode } : {}),
886
+ ...(errorDetail ? { errorDetail } : {}),
887
+ ...(resolved.effectiveModel === "unknown" ? { modelResolution: "unresolved-alias" } : {}),
888
+ ...(payload.structured_output !== undefined ? { structuredOutput: payload.structured_output } : {}),
889
+ };
890
+ }
891
+
892
+ function resolveHelperCostBasis(explicit, providerReportedCostUsd) {
893
+ if (explicit === "list") return "list";
894
+ if (explicit === "unmetered") return "unmetered";
895
+ if (providerReportedCostUsd === undefined) return "unknown";
896
+ return "provider-reported";
897
+ }
898
+
899
+ const CLAIM_SIDECAR_KEYS = Object.freeze([
900
+ "summary",
901
+ "deferredItems",
902
+ "findings",
903
+ "notes",
904
+ "notes_for_next_agent",
905
+ "notesForNextAgent",
906
+ "report",
907
+ "lists",
908
+ ]);
909
+ const CLAIM_COUNT_SOURCE_KEYS = Object.freeze(["completed", "deferred", "artifacts"]);
910
+ const CLAIM_CLOSED_KEYS = Object.freeze([
911
+ "status",
912
+ "completedCount",
913
+ "deferredCount",
914
+ "artifactCount",
915
+ "verdict",
916
+ ]);
917
+ const RESULT_PUBLIC_KEYS = Object.freeze([
918
+ "schema",
919
+ "ok",
920
+ "status",
921
+ "stage",
922
+ "harness",
923
+ "provider",
924
+ "role",
925
+ "requestedModel",
926
+ "effectiveModel",
927
+ "reasoningEffort",
928
+ "effortClampedFrom",
929
+ "startedAt",
930
+ "finishedAt",
931
+ "latencyMs",
932
+ "exitCode",
933
+ "signal",
934
+ "timedOut",
935
+ "killRequest",
936
+ "observedChildExit",
937
+ "tokenBasis",
938
+ "contextOccupancy",
939
+ "tokensIn",
940
+ "tokensOut",
941
+ "cacheReadTokens",
942
+ "cacheCreationTokens",
943
+ "reasoningTokens",
944
+ "totalTokens",
945
+ "costUsd",
946
+ "costBasis",
947
+ "providerReportedCostUsd",
948
+ "usagePartial",
949
+ "sessionId",
950
+ "stopReason",
951
+ "errorCode",
952
+ "error",
953
+ "auxiliaryUsage",
954
+ "usageEvents",
955
+ "candidateModels",
956
+ "auth",
957
+ "command",
958
+ "dispatchPath",
959
+ "answerPath",
960
+ "answerBytes",
961
+ "stderrPath",
962
+ "stderrBytes",
963
+ "errorPath",
964
+ "errorBytes",
965
+ "modelClaim",
966
+ "providerMetadata",
967
+ ]);
968
+ const MODEL_CLAIM_PUBLIC_KEYS = Object.freeze([
969
+ "status",
970
+ "completedCount",
971
+ "deferredCount",
972
+ "artifactCount",
973
+ "verdict",
974
+ "claimSource",
975
+ "unrecognizedCount",
976
+ "path",
977
+ "bytes",
978
+ ]);
979
+ const AUTH_PUBLIC_KEYS = Object.freeze(["loggedIn", "method", "observedAt", "subscriptionType"]);
980
+ const KILL_REQUEST_KEYS = Object.freeze(["signal", "escalated"]);
981
+ const USAGE_ROW_KEYS = Object.freeze([
982
+ "model",
983
+ "tokensIn",
984
+ "tokensOut",
985
+ "cacheReadTokens",
986
+ "cacheCreationTokens",
987
+ "costUsd",
988
+ "costBasis",
989
+ "stopReason",
990
+ ]);
991
+ const PROVIDER_METADATA_KEYS = Object.freeze([
992
+ "tokensIn",
993
+ "tokensOut",
994
+ "cacheReadTokens",
995
+ "cacheCreationTokens",
996
+ "reasoningTokens",
997
+ "totalTokens",
998
+ "tokenBasis",
999
+ ]);
1000
+
1001
+ function pickKeys(value, keys) {
1002
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1003
+ const out = {};
1004
+ for (const key of keys) {
1005
+ if (value[key] !== undefined) out[key] = value[key];
1006
+ }
1007
+ return out;
1008
+ }
1009
+
1010
+ function closedNonnegInt(value) {
1011
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
1012
+ return undefined;
1013
+ }
1014
+
1015
+ function closedNonnegFinite(value) {
1016
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
1017
+ return undefined;
1018
+ }
1019
+
1020
+ function closedString(value) {
1021
+ return typeof value === "string" ? value : undefined;
1022
+ }
1023
+
1024
+ function closedBoolean(value) {
1025
+ return value === true || value === false ? value : undefined;
1026
+ }
1027
+
1028
+ function closedCostBasis(value) {
1029
+ return typeof value === "string" && COST_BASIS.includes(value) ? value : undefined;
1030
+ }
1031
+
1032
+ const PUBLIC_STRING_KEYS = Object.freeze([
1033
+ "schema",
1034
+ "harness",
1035
+ "provider",
1036
+ "role",
1037
+ "requestedModel",
1038
+ "effectiveModel",
1039
+ "reasoningEffort",
1040
+ "effortClampedFrom",
1041
+ "startedAt",
1042
+ "finishedAt",
1043
+ "signal",
1044
+ "tokenBasis",
1045
+ "contextOccupancy",
1046
+ "sessionId",
1047
+ "error",
1048
+ "command",
1049
+ "dispatchPath",
1050
+ "answerPath",
1051
+ "stderrPath",
1052
+ "errorPath",
1053
+ ]);
1054
+ const PUBLIC_TOKEN_KEYS = Object.freeze([
1055
+ "tokensIn",
1056
+ "tokensOut",
1057
+ "cacheReadTokens",
1058
+ "cacheCreationTokens",
1059
+ "reasoningTokens",
1060
+ "totalTokens",
1061
+ ]);
1062
+ const PUBLIC_COST_KEYS = Object.freeze(["costUsd", "providerReportedCostUsd"]);
1063
+ const PUBLIC_BYTE_KEYS = Object.freeze(["answerBytes", "stderrBytes", "errorBytes"]);
1064
+
1065
+ function closeUsageRow(row) {
1066
+ const closed = pickKeys(row, USAGE_ROW_KEYS);
1067
+ if (!closed) return undefined;
1068
+ const model = closedString(closed.model);
1069
+ if (model === undefined) delete closed.model;
1070
+ else closed.model = model;
1071
+ for (const key of PUBLIC_TOKEN_KEYS) {
1072
+ if (!(key in closed)) continue;
1073
+ const value = closedNonnegInt(closed[key]);
1074
+ if (value === undefined) delete closed[key];
1075
+ else closed[key] = value;
1076
+ }
1077
+ const costUsd = closedNonnegFinite(closed.costUsd);
1078
+ if (costUsd === undefined) delete closed.costUsd;
1079
+ else closed.costUsd = costUsd;
1080
+ const costBasis = closedCostBasis(closed.costBasis);
1081
+ if (costBasis === undefined) delete closed.costBasis;
1082
+ else closed.costBasis = costBasis;
1083
+ if (closed.stopReason !== undefined) {
1084
+ const stopReason = closeStopReason(closed.stopReason);
1085
+ if (stopReason === undefined) delete closed.stopReason;
1086
+ else closed.stopReason = stopReason;
1087
+ }
1088
+ return Object.keys(closed).length > 0 ? closed : undefined;
1089
+ }
1090
+
1091
+ function closePublicResult(result) {
1092
+ const closed = pickKeys(result, RESULT_PUBLIC_KEYS) ?? {};
1093
+ closed.schema = RESULT_SCHEMA;
1094
+ if (closed.stopReason !== undefined) closed.stopReason = closeStopReason(closed.stopReason);
1095
+ if (closed.errorCode !== undefined && !ERROR_CODES.includes(closed.errorCode)) {
1096
+ closed.errorCode = "unrecognized";
1097
+ }
1098
+ if (closed.status !== undefined && !TRANSPORT_STATUSES.includes(closed.status)) {
1099
+ closed.status = "failed";
1100
+ }
1101
+ if (closed.stage !== undefined && !TRANSPORT_STAGES.includes(closed.stage)) {
1102
+ closed.stage = "run";
1103
+ }
1104
+ const ok = closedBoolean(closed.ok);
1105
+ if (ok === undefined) delete closed.ok;
1106
+ else closed.ok = ok;
1107
+ const timedOut = closedBoolean(closed.timedOut);
1108
+ if (timedOut === undefined) delete closed.timedOut;
1109
+ else closed.timedOut = timedOut;
1110
+ const observedChildExit = closedBoolean(closed.observedChildExit);
1111
+ if (observedChildExit === undefined) delete closed.observedChildExit;
1112
+ else closed.observedChildExit = observedChildExit;
1113
+ if (closed.usagePartial === true) closed.usagePartial = true;
1114
+ else delete closed.usagePartial;
1115
+ if (closed.exitCode !== undefined && closed.exitCode !== null
1116
+ && !(typeof closed.exitCode === "number" && Number.isInteger(closed.exitCode))) {
1117
+ delete closed.exitCode;
1118
+ }
1119
+ const latencyMs = closedNonnegFinite(closed.latencyMs);
1120
+ if (latencyMs === undefined) delete closed.latencyMs;
1121
+ else closed.latencyMs = latencyMs;
1122
+ for (const key of PUBLIC_STRING_KEYS) {
1123
+ if (closed[key] === undefined) continue;
1124
+ const value = closedString(closed[key]);
1125
+ if (value === undefined) delete closed[key];
1126
+ else closed[key] = value;
1127
+ }
1128
+ for (const key of PUBLIC_TOKEN_KEYS) {
1129
+ if (closed[key] === undefined) continue;
1130
+ const value = closedNonnegInt(closed[key]);
1131
+ if (value === undefined) delete closed[key];
1132
+ else closed[key] = value;
1133
+ }
1134
+ for (const key of PUBLIC_COST_KEYS) {
1135
+ if (closed[key] === undefined) continue;
1136
+ const value = closedNonnegFinite(closed[key]);
1137
+ if (value === undefined) delete closed[key];
1138
+ else closed[key] = value;
1139
+ }
1140
+ for (const key of PUBLIC_BYTE_KEYS) {
1141
+ if (closed[key] === undefined) continue;
1142
+ const value = closedNonnegInt(closed[key]);
1143
+ if (value === undefined) delete closed[key];
1144
+ else closed[key] = value;
1145
+ }
1146
+ const costBasis = closedCostBasis(closed.costBasis);
1147
+ if (costBasis === undefined) delete closed.costBasis;
1148
+ else closed.costBasis = costBasis;
1149
+ if (closed.tokenBasis !== undefined && closed.tokenBasis !== TOKEN_BASIS) delete closed.tokenBasis;
1150
+ if (closed.contextOccupancy !== undefined && closed.contextOccupancy !== CONTEXT_OCCUPANCY_UNKNOWN) {
1151
+ delete closed.contextOccupancy;
1152
+ }
1153
+ if (closed.modelClaim) {
1154
+ closed.modelClaim = pickKeys(closed.modelClaim, MODEL_CLAIM_PUBLIC_KEYS);
1155
+ if (
1156
+ closed.modelClaim.status !== undefined
1157
+ && !MODEL_CLAIM_STATUSES.includes(closed.modelClaim.status)
1158
+ && closed.modelClaim.status !== "unrecognized"
1159
+ ) {
1160
+ closed.modelClaim.status = "unrecognized";
1161
+ }
1162
+ if (typeof closed.modelClaim.status !== "string") delete closed.modelClaim.status;
1163
+ if (closed.modelClaim.verdict !== undefined && !REVIEW_VERDICTS.includes(closed.modelClaim.verdict)) {
1164
+ delete closed.modelClaim.verdict;
1165
+ }
1166
+ if (closed.modelClaim.claimSource !== undefined && !CLAIM_SOURCES.includes(closed.modelClaim.claimSource)) {
1167
+ closed.modelClaim.claimSource = "none";
1168
+ }
1169
+ for (const key of ["completedCount", "deferredCount", "artifactCount", "unrecognizedCount", "bytes"]) {
1170
+ const value = closedNonnegInt(closed.modelClaim[key]);
1171
+ if (value === undefined) delete closed.modelClaim[key];
1172
+ else closed.modelClaim[key] = value;
1173
+ }
1174
+ const claimPath = closedString(closed.modelClaim.path);
1175
+ if (claimPath === undefined) delete closed.modelClaim.path;
1176
+ else closed.modelClaim.path = claimPath;
1177
+ }
1178
+ if (closed.auth) {
1179
+ closed.auth = pickKeys(closed.auth, AUTH_PUBLIC_KEYS);
1180
+ const loggedIn = closedBoolean(closed.auth.loggedIn);
1181
+ if (loggedIn === undefined) delete closed.auth.loggedIn;
1182
+ else closed.auth.loggedIn = loggedIn;
1183
+ const method = closedString(closed.auth.method);
1184
+ if (method === undefined) delete closed.auth.method;
1185
+ else closed.auth.method = method;
1186
+ const observedAt = closedString(closed.auth.observedAt);
1187
+ if (observedAt === undefined) delete closed.auth.observedAt;
1188
+ else closed.auth.observedAt = observedAt;
1189
+ const subscriptionType = closed.auth.subscriptionType;
1190
+ if (subscriptionType !== undefined
1191
+ && typeof subscriptionType !== "string"
1192
+ && typeof subscriptionType !== "number"
1193
+ && typeof subscriptionType !== "boolean") {
1194
+ delete closed.auth.subscriptionType;
1195
+ }
1196
+ }
1197
+ if (closed.killRequest) {
1198
+ closed.killRequest = pickKeys(closed.killRequest, KILL_REQUEST_KEYS);
1199
+ const killSignal = closedString(closed.killRequest.signal);
1200
+ if (killSignal === undefined) delete closed.killRequest.signal;
1201
+ else closed.killRequest.signal = killSignal;
1202
+ closed.killRequest.escalated = closed.killRequest.escalated === true;
1203
+ }
1204
+ if (Array.isArray(closed.auxiliaryUsage)) {
1205
+ closed.auxiliaryUsage = closed.auxiliaryUsage.map(closeUsageRow).filter(Boolean);
1206
+ } else {
1207
+ delete closed.auxiliaryUsage;
1208
+ }
1209
+ if (Array.isArray(closed.usageEvents)) {
1210
+ closed.usageEvents = closed.usageEvents.map(closeUsageRow).filter(Boolean);
1211
+ } else {
1212
+ delete closed.usageEvents;
1213
+ }
1214
+ if (closed.providerMetadata) {
1215
+ closed.providerMetadata = pickKeys(closed.providerMetadata, PROVIDER_METADATA_KEYS);
1216
+ if (closed.providerMetadata) {
1217
+ for (const key of PUBLIC_TOKEN_KEYS) {
1218
+ if (closed.providerMetadata[key] === undefined) continue;
1219
+ const value = closedNonnegInt(closed.providerMetadata[key]);
1220
+ if (value === undefined) delete closed.providerMetadata[key];
1221
+ else closed.providerMetadata[key] = value;
1222
+ }
1223
+ if (closed.providerMetadata.tokenBasis !== undefined
1224
+ && closed.providerMetadata.tokenBasis !== TOKEN_BASIS) {
1225
+ delete closed.providerMetadata.tokenBasis;
1226
+ }
1227
+ if (Object.keys(closed.providerMetadata).length === 0) delete closed.providerMetadata;
1228
+ }
1229
+ }
1230
+ if (Array.isArray(closed.candidateModels)) {
1231
+ closed.candidateModels = closed.candidateModels.filter((name) => typeof name === "string");
1232
+ } else {
1233
+ delete closed.candidateModels;
1234
+ }
1235
+ if (closed.signal == null) delete closed.signal;
1236
+ delete closed.stderr;
1237
+ delete closed.stdout;
1238
+ delete closed.text;
1239
+ delete closed.argv;
1240
+ delete closed.harnessError;
1241
+ assertClosedResult(closed);
1242
+ return closed;
1243
+ }
1244
+
1245
+ function assertClosedResult(result) {
1246
+ for (const key of Object.keys(result)) {
1247
+ if (!RESULT_PUBLIC_KEYS.includes(key)) {
1248
+ throw failClosed(`result leaked unknown field ${key}`, "run");
1249
+ }
1250
+ }
1251
+ if (Object.hasOwn(result, "stderr") || Object.hasOwn(result, "stdout") || Object.hasOwn(result, "text")) {
1252
+ throw failClosed("structured result must not carry raw stdio");
1253
+ }
1254
+ if (Object.hasOwn(result, "harnessError")) {
1255
+ throw failClosed("structured result must not carry raw harnessError text");
1256
+ }
1257
+ if (Object.hasOwn(result, "contextTokens")) {
1258
+ throw failClosed("contextTokens is removed; occupancy is unknown");
1259
+ }
1260
+ }
1261
+
1262
+ function capClaimCount(value) {
1263
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
1264
+ return Math.min(value, CLAIM_COUNT_CAP);
1265
+ }
1266
+ return undefined;
1267
+ }
1268
+
1269
+ function claimCount(value) {
1270
+ if (Array.isArray(value)) return Math.min(value.length, CLAIM_COUNT_CAP);
1271
+ return capClaimCount(value);
1272
+ }
1273
+
1274
+ function extractModelClaimFromObject(candidate, role) {
1275
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return undefined;
1276
+ if (typeof candidate.status !== "string" && candidate.verdict === undefined
1277
+ && candidate.completed === undefined && candidate.deferred === undefined
1278
+ && candidate.artifacts === undefined && candidate.completedCount === undefined) {
1279
+ return undefined;
1280
+ }
1281
+
1282
+ const known = new Set([...CLAIM_CLOSED_KEYS, ...CLAIM_SIDECAR_KEYS, ...CLAIM_COUNT_SOURCE_KEYS]);
1283
+ let unrecognizedCount = 0;
1284
+ for (const key of Object.keys(candidate)) {
1285
+ if (!known.has(key)) unrecognizedCount += 1;
1286
+ }
1287
+
1288
+ const rawStatus = candidate.status;
1289
+ const status = MODEL_CLAIM_STATUSES.includes(rawStatus) ? rawStatus : "unrecognized";
1290
+ if (typeof rawStatus === "string" && !MODEL_CLAIM_STATUSES.includes(rawStatus)) {
1291
+ unrecognizedCount += 1;
1292
+ } else if (rawStatus !== undefined && typeof rawStatus !== "string") {
1293
+ unrecognizedCount += 1;
1294
+ }
1295
+
1296
+ const completedCount = claimCount(candidate.completedCount ?? candidate.completed);
1297
+ const deferredCount = claimCount(candidate.deferredCount ?? candidate.deferred);
1298
+ const artifactCount = claimCount(candidate.artifactCount ?? candidate.artifacts);
1299
+
1300
+ const review = role === "reviewer-arch" || role === "reviewer-cli";
1301
+ let verdict;
1302
+ if (candidate.verdict !== undefined) {
1303
+ if (review && REVIEW_VERDICTS.includes(candidate.verdict)) {
1304
+ verdict = candidate.verdict;
1305
+ } else {
1306
+ unrecognizedCount += 1;
1307
+ }
1308
+ }
1309
+
1310
+ return {
1311
+ status,
1312
+ ...(completedCount !== undefined ? { completedCount } : {}),
1313
+ ...(deferredCount !== undefined ? { deferredCount } : {}),
1314
+ ...(artifactCount !== undefined ? { artifactCount } : {}),
1315
+ ...(verdict ? { verdict } : {}),
1316
+ unrecognizedCount,
1317
+ raw: candidate,
1318
+ };
1319
+ }
1320
+
1321
+ function extractModelClaim(text, role) {
1322
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
1323
+ const candidate = parseJson(fenced ? fenced[1] : text);
1324
+ return extractModelClaimFromObject(candidate, role);
1325
+ }
1326
+
1327
+ function extractClaim(fields, role, schemaRequested) {
1328
+ if (schemaRequested && fields?.structuredOutput !== undefined) {
1329
+ const raw = fields.structuredOutput;
1330
+ const extracted = extractModelClaimFromObject(raw, role);
1331
+ if (extracted) return { ...extracted, claimSource: "structured_output" };
1332
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
1333
+ return {
1334
+ status: "unrecognized",
1335
+ unrecognizedCount: Object.keys(raw).length,
1336
+ claimSource: "structured_output",
1337
+ raw,
1338
+ };
1339
+ }
1340
+ return {
1341
+ status: "unrecognized",
1342
+ unrecognizedCount: 1,
1343
+ claimSource: "structured_output",
1344
+ raw: { structured_output: raw },
1345
+ };
1346
+ }
1347
+ const fromText = extractModelClaim(typeof fields?.text === "string" ? fields.text : "", role);
1348
+ if (fromText) return { ...fromText, claimSource: "result" };
1349
+ return undefined;
1350
+ }
1351
+
1352
+ export function diagnoseHarnessResult(payload, filePath = "-") {
1353
+ const rawObserved = payload && typeof payload === "object" && !Array.isArray(payload)
1354
+ ? payload.schema
1355
+ : undefined;
1356
+ const observed = typeof rawObserved === "string" && rawObserved.length > 0 ? rawObserved : "none";
1357
+ if (observed === RESULT_SCHEMA) return { result: payload };
1358
+ const observedLabel = observed === "none" || KNOWN_RESULT_SCHEMAS.includes(observed)
1359
+ ? observed
1360
+ : "unrecognized";
1361
+ return {
1362
+ diagnostic: `${filePath}: observed schema ${observedLabel}; obsolete result schema ${OBSOLETE_RESULT_SCHEMA}; expected ${RESULT_SCHEMA}`,
1363
+ };
1364
+ }
1365
+
1366
+ export function formatRunListing(payload, filePath = "-") {
1367
+ const diagnosed = diagnoseHarnessResult(payload, filePath);
1368
+ if (diagnosed.diagnostic) return diagnosed.diagnostic;
1369
+ const row = diagnosed.result;
1370
+ const cost = formatRunCost(row);
1371
+ return `${row.ok ? "ok" : "FAIL"} ${row.harness ?? ""} ${row.effectiveModel ?? ""} ${(row.latencyMs ?? "?") + "ms"} ${cost}`;
1372
+ }
1373
+
1374
+ function applyRoutingCap(fields) {
1375
+ const metadata = {};
1376
+ const capped = {};
1377
+ for (const name of PUBLIC_TOKEN_KEYS) {
1378
+ const value = closedNonnegInt(fields[name]);
1379
+ if (value === undefined) continue;
1380
+ if (value > ROUTING_INT_CAP) {
1381
+ metadata[name] = value;
1382
+ metadata.tokenBasis = TOKEN_BASIS;
1383
+ } else {
1384
+ capped[name] = value;
1385
+ }
1386
+ }
1387
+ return { capped, metadata };
1388
+ }
1389
+
1390
+ function requireNonemptyString(request, key) {
1391
+ const value = request[key];
1392
+ if (typeof value !== "string" || value.trim() === "") {
1393
+ throw failClosed(`${key} is required as a nonempty string; refusing CLI defaults`);
1394
+ }
1395
+ }
1396
+
1397
+ function optionalNonemptyString(request, key) {
1398
+ if (request[key] === undefined) return;
1399
+ if (typeof request[key] !== "string" || request[key].length === 0) {
1400
+ throw failClosed(`${key} must be a nonempty string when present`);
1401
+ }
1402
+ }
1403
+
1404
+ function requirePositiveFinite(request, key) {
1405
+ if (request[key] === undefined) return;
1406
+ const value = request[key];
1407
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
1408
+ throw failClosed(`${key} must be a positive finite number; zero is not a verified constraint`);
1409
+ }
1410
+ }
1411
+
1412
+ export function preflightRequest(request) {
1413
+ if (!request || typeof request !== "object" || Array.isArray(request)) {
1414
+ throw failClosed("request must be an object");
1415
+ }
1416
+ if (request.schema !== REQUEST_SCHEMA) {
1417
+ throw failClosed(`request schema must be ${REQUEST_SCHEMA}, got ${request.schema ?? "none"}`);
1418
+ }
1419
+ const unknown = Object.keys(request).filter((key) => !REQUEST_KEYS.includes(key));
1420
+ if (unknown.length > 0) {
1421
+ throw failClosed(`unknown request field(s) ${unknown.join(", ")}; refusing to guess a route`);
1422
+ }
1423
+ for (const key of ["harness", "role", "model", "permission", "prompt_file"]) {
1424
+ requireNonemptyString(request, key);
1425
+ }
1426
+ for (const key of ["cwd", "output_schema", "output_dir"]) {
1427
+ optionalNonemptyString(request, key);
1428
+ }
1429
+ if (request.effort !== undefined && (typeof request.effort !== "string" || !request.effort)) {
1430
+ throw failClosed("effort must be a nonempty string when present");
1431
+ }
1432
+ if (request.hooks !== undefined && typeof request.hooks !== "boolean") {
1433
+ throw failClosed("hooks must be a boolean when present");
1434
+ }
1435
+ if (request.skills !== undefined) {
1436
+ if (!Array.isArray(request.skills) || request.skills.some((skill) => typeof skill !== "string" || skill.length === 0)) {
1437
+ throw failClosed("skills must be an array of nonempty strings when present");
1438
+ }
1439
+ }
1440
+ requirePositiveFinite(request, "timeout_ms");
1441
+ requirePositiveFinite(request, "max_cost_usd");
1442
+ const harness = request.harness;
1443
+ if (UNVERIFIED_HARNESSES.includes(harness)) {
1444
+ throw failClosed(`${harness} helper dispatch is unverified and fails closed before any model call`);
1445
+ }
1446
+ const route = ROUTES[harness];
1447
+ if (!route) {
1448
+ throw failClosed(`unknown harness ${harness}; expected one of ${Object.keys(ROUTES).join(", ")}`);
1449
+ }
1450
+ if (!route.roles.includes(request.role)) {
1451
+ throw failClosed(`${harness} does not accept role ${request.role}; allowed: ${route.roles.join(", ")}`);
1452
+ }
1453
+ if (harness === "grok" && request.permission === "read-only") {
1454
+ throw failClosed("grok read-only is unsupported pending a permission-mode probe");
1455
+ }
1456
+ if (!route.permissions.includes(request.permission)) {
1457
+ throw failClosed(`${harness} does not accept permission ${request.permission}; allowed: ${route.permissions.join(", ")}`);
1458
+ }
1459
+ if (harness === "pi" && request.permission === "edit" && !["experiment", "writer"].includes(request.role)) {
1460
+ throw failClosed(`pi ${request.role} cannot edit; only experiment or admitted writer may use permission edit`);
1461
+ }
1462
+ if (harness !== "pi" && !route.models.includes(request.model)) {
1463
+ throw failClosed(`${harness} does not accept model ${request.model}; allowed: ${route.models.join(", ")}`);
1464
+ }
1465
+ if (harness === "pi") {
1466
+ const provider = piProviderOf(request.model);
1467
+ if (!provider) {
1468
+ throw failClosed("pi model must be provider/id (openrouter/* or nous-portal/* in this helper)");
1469
+ }
1470
+ if (NATIVE_PI_BRAKE_PROVIDERS.includes(provider)) {
1471
+ throw failClosed(`pi brake: ${provider} has a native harness; refusing Pi impersonation`);
1472
+ }
1473
+ if (!PI_ALLOWED_PROVIDERS.includes(provider)) {
1474
+ throw failClosed(`pi helper allows only openrouter/* or nous-portal/*; ${provider} is not a native CLI and is not allowlisted`);
1475
+ }
1476
+ if (request.role === "writer" && (request.model !== PI_ADMITTED_WRITER || request.permission !== "edit")) {
1477
+ throw failClosed("pi writer refuses unsupported route; allows only authenticated openrouter/qwen/qwen3-coder-plus with edit permission; other models need reviewed route admission");
1478
+ }
1479
+ }
1480
+ if (harness === "claude") {
1481
+ if (request.hooks === true) {
1482
+ throw failClosed("read-only claude refuses hooks; it runs --safe-mode with no hook/skill loading");
1483
+ }
1484
+ if (Array.isArray(request.skills) && request.skills.length > 0) {
1485
+ throw failClosed("read-only claude refuses skills; it runs --safe-mode with no hook/skill loading");
1486
+ }
1487
+ } else if (harness === "pi") {
1488
+ if (request.hooks === true) {
1489
+ throw failClosed("pi does not accept hooks:true; read-only uses --no-extensions");
1490
+ }
1491
+ if (Array.isArray(request.skills) && request.skills.length > 0) {
1492
+ throw failClosed("pi does not accept skills in this helper");
1493
+ }
1494
+ } else if (request.hooks !== undefined || request.skills !== undefined) {
1495
+ throw failClosed(`${harness} does not accept hooks/skills in this helper`);
1496
+ }
1497
+ if (request.max_cost_usd !== undefined && harness !== "claude") {
1498
+ throw failClosed(`${harness} does not accept max_cost_usd in this helper`);
1499
+ }
1500
+ if (request.max_turns !== undefined) {
1501
+ const value = request.max_turns;
1502
+ if (!Number.isInteger(value) || value <= 0) {
1503
+ throw failClosed("max_turns must be a positive integer; zero is not a verified constraint");
1504
+ }
1505
+ if (harness !== "grok") {
1506
+ throw failClosed(`${harness} does not accept max_turns in this helper`);
1507
+ }
1508
+ }
1509
+ return route;
1510
+ }
1511
+
1512
+ function writePrivate(path, body, options = {}) {
1513
+ const write = options.writeFileSync ?? writeFileSync;
1514
+ const chmod = options.chmodSync ?? chmodSync;
1515
+ write(path, body, { encoding: "utf8", mode: 0o600 });
1516
+ try {
1517
+ chmod(path, 0o600);
1518
+ } catch {
1519
+ // Windows cannot honor POSIX 0600; the create mode still ran.
1520
+ }
1521
+ }
1522
+
1523
+ function ensureOutputDir(dir, options = {}) {
1524
+ const mkdir = options.mkdirSync ?? mkdirSync;
1525
+ mkdir(dir, { recursive: true, mode: 0o700 });
1526
+ return dir;
1527
+ }
1528
+
1529
+ function emptyMcpConfig(dir, options = {}) {
1530
+ const path = join(dir, "mcp-empty.json");
1531
+ writePrivate(path, `${JSON.stringify({ mcpServers: {} })}\n`, options);
1532
+ return path;
1533
+ }
1534
+
1535
+ function readResolvedInput(label, relativePath) {
1536
+ const resolved = resolvePath(process.cwd(), relativePath);
1537
+ try {
1538
+ return { path: resolved, text: readFileSync(resolved, "utf8") };
1539
+ } catch {
1540
+ throw failClosed(`${label} is missing or unreadable: ${resolved}`);
1541
+ }
1542
+ }
1543
+
1544
+ export async function runHarness(request, deps = {}) {
1545
+ const platform = deps.platform ?? process.platform;
1546
+ const spawnImpl = deps.spawn ?? spawn;
1547
+ const spawnSyncImpl = deps.spawnSync ?? spawnSync;
1548
+ const now = deps.now ?? Date.now;
1549
+ const exists = deps.existsSync ?? existsSync;
1550
+ const route = preflightRequest(request);
1551
+ // Request file paths resolve against the invocation cwd, then become
1552
+ // absolute before the child runs in request.cwd, so a brief in the
1553
+ // control repo works while the agent runs in a worktree.
1554
+ const promptInput = readResolvedInput("prompt_file", request.prompt_file);
1555
+ const schemaInput = request.output_schema
1556
+ ? readResolvedInput("output_schema", request.output_schema)
1557
+ : undefined;
1558
+ const cwd = request.cwd ?? process.cwd();
1559
+ const env = deps.env ?? process.env;
1560
+ const { effort, clamped } = clampEffort(request.harness, request.effort);
1561
+ const prepared = { ...request, effort };
1562
+ const outputDir = ensureOutputDir(
1563
+ request.output_dir ?? join(cwd, ".kxm", "logs", `harness-${now()}`),
1564
+ deps,
1565
+ );
1566
+ const mcpConfigPath = request.harness === "claude" ? emptyMcpConfig(outputDir, deps) : undefined;
1567
+ const argv = buildArgv(prepared, {
1568
+ mcpConfigPath,
1569
+ promptPath: promptInput.path,
1570
+ promptText: promptInput.text,
1571
+ schemaPath: schemaInput?.path,
1572
+ schemaText: schemaInput?.text,
1573
+ });
1574
+ const cliId = request.harness;
1575
+ const resolved = resolveLauncher(cliId, {
1576
+ platform,
1577
+ pathEnv: env.PATH,
1578
+ existsSync: exists,
1579
+ realpathSync: deps.realpathSync,
1580
+ });
1581
+
1582
+ const authArgs = request.harness === "claude"
1583
+ ? [...claudeIsolationArgs(mcpConfigPath), ...route.auth.args]
1584
+ : request.harness === "pi"
1585
+ ? piAuthCheckArgs(request)
1586
+ : [...route.auth.args];
1587
+ const auth = spawnSyncImpl(resolved, authArgs, {
1588
+ cwd,
1589
+ env,
1590
+ encoding: "utf8",
1591
+ shell: false,
1592
+ windowsHide: true,
1593
+ timeout: deps.authTimeoutMs ?? 15_000,
1594
+ });
1595
+ if (auth.error && auth.error.code === "ENOENT") {
1596
+ throw failClosed(loginHint(cliId, `missing binary ${cliId}.`), "auth");
1597
+ }
1598
+ let authStatus;
1599
+ authStatus = parseAuth(cliId, {
1600
+ stdout: auth.stdout?.toString?.() ?? "",
1601
+ stderr: auth.stderr?.toString?.() ?? "",
1602
+ exitCode: auth.status ?? (auth.error ? -1 : 0),
1603
+ }, {
1604
+ observedAt: deps.observedAt,
1605
+ ...(request.harness === "pi" ? { provider: piProviderOf(request.model) } : {}),
1606
+ });
1607
+
1608
+ if (request.harness === "pi" && request.role === "writer") {
1609
+ const proof = parseJson(auth.stdout?.toString?.() ?? "");
1610
+ if (proof?.provider !== piProviderOf(request.model) || proof?.status !== "ready") {
1611
+ throw failClosed("pi writer requires exact provider/model auth readiness", "auth");
1612
+ }
1613
+ }
1614
+
1615
+ const stdinPrompt = request.harness === "claude" || request.harness === "codex";
1616
+ const started = now();
1617
+ const dispatchRecord = {
1618
+ requestHash: createHash("sha256").update(JSON.stringify(request)).digest("hex"),
1619
+ timestamp: new Date(started).toISOString(),
1620
+ command: cliId,
1621
+ pid: null,
1622
+ };
1623
+ const dispatchPath = join(outputDir, "dispatch.json");
1624
+ writePrivate(dispatchPath, `${JSON.stringify(dispatchRecord)}\n`, deps);
1625
+
1626
+ const killGraceMs = deps.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
1627
+ let timedOut = false;
1628
+ let killRequest;
1629
+ let child;
1630
+ try {
1631
+ child = spawnImpl(resolved, argv, {
1632
+ cwd,
1633
+ env,
1634
+ shell: false,
1635
+ windowsHide: true,
1636
+ stdio: [stdinPrompt ? "pipe" : "ignore", "pipe", "pipe"],
1637
+ });
1638
+ } catch {
1639
+ return closePublicResult(buildTransportResult({
1640
+ request,
1641
+ route,
1642
+ effort,
1643
+ clamped,
1644
+ authStatus,
1645
+ started,
1646
+ latencyMs: now() - started,
1647
+ collected: { stdout: "", stderr: "", exitCode: null, signal: null, observedChildExit: false },
1648
+ fields: { costBasis: "unknown" },
1649
+ timedOut: false,
1650
+ killRequest: undefined,
1651
+ outputDir,
1652
+ dispatchPath,
1653
+ cliId,
1654
+ stage: "spawn",
1655
+ errorCode: "spawn_failed",
1656
+ errorDetail: "spawn failed",
1657
+ writeFailed: false,
1658
+ modelClaim: undefined,
1659
+ answerBytes: 0,
1660
+ stderrBytes: 0,
1661
+ errorBytes: 0,
1662
+ }));
1663
+ }
1664
+
1665
+ let writeFailed = false;
1666
+ let writeDetail;
1667
+ const requestTermination = (signal) => {
1668
+ killRequest = { signal, escalated: signal === "SIGKILL" };
1669
+ try {
1670
+ child.kill(signal);
1671
+ } catch {
1672
+ // Direct child may already have exited; missing completion is not process-death proof.
1673
+ }
1674
+ };
1675
+ const collectedPromise = collectChildStdio(child, {
1676
+ timeoutMs: request.timeout_ms,
1677
+ killGraceMs,
1678
+ onTimeout: () => {
1679
+ timedOut = true;
1680
+ },
1681
+ requestTermination,
1682
+ });
1683
+ try {
1684
+ if (child?.pid != null) {
1685
+ dispatchRecord.pid = child.pid;
1686
+ writePrivate(dispatchPath, `${JSON.stringify(dispatchRecord)}\n`, deps);
1687
+ }
1688
+ } catch (error) {
1689
+ writeFailed = true;
1690
+ writeDetail = String(error?.message ?? "pid record amend failed");
1691
+ }
1692
+ if (stdinPrompt && child.stdin) {
1693
+ const noteStdinFailure = (error) => {
1694
+ writeFailed = true;
1695
+ writeDetail = joinDetail(writeDetail, String(error?.message ?? "stdin write failed"));
1696
+ };
1697
+ child.stdin.on("error", noteStdinFailure);
1698
+ try {
1699
+ child.stdin.write(promptInput.text);
1700
+ child.stdin.end();
1701
+ } catch (error) {
1702
+ noteStdinFailure(error);
1703
+ }
1704
+ }
1705
+ const collected = await collectedPromise;
1706
+ const latencyMs = now() - started;
1707
+ const spawnFailed = Boolean(collected.error) && collected.observedChildExit !== true;
1708
+
1709
+ let fields;
1710
+ try {
1711
+ fields = request.harness === "codex"
1712
+ ? normalizeCodex(collected.stdout)
1713
+ : request.harness === "pi"
1714
+ ? normalizePi(collected.stdout)
1715
+ : request.harness === "agy"
1716
+ ? normalizeAgy(parseJson(collected.stdout))
1717
+ : normalizeClaudeOrGrok(parseJson(collected.stdout), request.model);
1718
+ } catch (error) {
1719
+ fields = {
1720
+ errorCode: "normalization_failed",
1721
+ errorDetail: String(error?.message ?? "normalization failed"),
1722
+ costBasis: "unknown",
1723
+ };
1724
+ }
1725
+
1726
+ if (request.harness === "claude" && authStatus.method === "claude.ai") {
1727
+ fields.costBasis = "unmetered";
1728
+ fields.costUsd = undefined;
1729
+ } else if (request.harness === "codex" && authStatus.method === "ChatGPT") {
1730
+ fields.costBasis = "unmetered";
1731
+ fields.costUsd = undefined;
1732
+ } else if (request.harness === "agy" && authStatus.method === "antigravity-oauth") {
1733
+ fields.costBasis = "unmetered";
1734
+ fields.costUsd = undefined;
1735
+ } else if (fields.costUsd === undefined && fields.providerReportedCostUsd === undefined) {
1736
+ fields.costBasis = fields.costBasis ?? "unknown";
1737
+ }
1738
+ if (fields.costBasis === "billed" || (fields.costBasis && !COST_BASIS.includes(fields.costBasis))) {
1739
+ fields.costBasis = fields.providerReportedCostUsd !== undefined || fields.costUsd !== undefined
1740
+ ? "provider-reported"
1741
+ : "unknown";
1742
+ }
1743
+
1744
+ const stderrPath = join(outputDir, "stderr.log");
1745
+ const answerPath = join(outputDir, "answer.txt");
1746
+ const errorPath = join(outputDir, "error.txt");
1747
+ const stderrBytes = Buffer.byteLength(collected.stderr ?? "", "utf8");
1748
+ const answerText = typeof fields.text === "string" ? fields.text : "";
1749
+ const answerBytes = Buffer.byteLength(answerText, "utf8");
1750
+ let extractedClaim;
1751
+ let modelClaim;
1752
+ try {
1753
+ if (stderrBytes > 0) writePrivate(stderrPath, collected.stderr, deps);
1754
+ if (answerBytes > 0) writePrivate(answerPath, answerText, deps);
1755
+ extractedClaim = extractClaim(fields, request.role, Boolean(schemaInput));
1756
+ if (extractedClaim) {
1757
+ const claimPath = join(outputDir, "model-claim.json");
1758
+ const claimBody = `${JSON.stringify(extractedClaim.raw)}\n`;
1759
+ writePrivate(claimPath, claimBody, deps);
1760
+ modelClaim = {
1761
+ status: extractedClaim.status,
1762
+ ...(extractedClaim.completedCount !== undefined ? { completedCount: extractedClaim.completedCount } : {}),
1763
+ ...(extractedClaim.deferredCount !== undefined ? { deferredCount: extractedClaim.deferredCount } : {}),
1764
+ ...(extractedClaim.artifactCount !== undefined ? { artifactCount: extractedClaim.artifactCount } : {}),
1765
+ ...(extractedClaim.verdict ? { verdict: extractedClaim.verdict } : {}),
1766
+ claimSource: extractedClaim.claimSource ?? "none",
1767
+ unrecognizedCount: extractedClaim.unrecognizedCount,
1768
+ path: claimPath,
1769
+ bytes: Buffer.byteLength(claimBody),
1770
+ };
1771
+ }
1772
+ } catch (error) {
1773
+ writeFailed = true;
1774
+ writeDetail = joinDetail(writeDetail, String(error?.message ?? "sidecar write failed"));
1775
+ }
1776
+
1777
+ const emptyPayload = fields.emptyPayload === true
1778
+ || ((request.harness === "codex" || request.harness === "pi")
1779
+ ? jsonlEvents(collected.stdout).length === 0
1780
+ : !parseJson(collected.stdout));
1781
+ let errorCode = fields.errorCode;
1782
+ let errorDetail = fields.errorDetail;
1783
+ if (timedOut) {
1784
+ errorCode = "timed_out";
1785
+ errorDetail = joinDetail(errorDetail, "timed out");
1786
+ } else if (collected.signal && collected.observedChildExit) {
1787
+ errorCode = errorCode ?? "signaled";
1788
+ errorDetail = joinDetail(errorDetail, `signal ${collected.signal}`);
1789
+ } else if (spawnFailed) {
1790
+ errorCode = "spawn_failed";
1791
+ errorDetail = joinDetail(errorDetail, String(collected.error?.message ?? "spawn failed"));
1792
+ } else if (collected.outputComplete !== true) {
1793
+ errorCode = "stdio_incomplete";
1794
+ errorDetail = joinDetail(errorDetail, "stdio incomplete");
1795
+ } else if (collected.observedChildExit === true && collected.exitCode === null && !collected.signal) {
1796
+ errorCode = errorCode ?? "unknown_exit";
1797
+ errorDetail = joinDetail(errorDetail, "unknown exit");
1798
+ } else if (emptyPayload && !errorCode) {
1799
+ errorCode = "empty_payload";
1800
+ errorDetail = errorDetail ?? "empty JSON payload";
1801
+ }
1802
+ if (writeFailed) {
1803
+ errorCode = errorCode ?? "write_failed";
1804
+ errorDetail = joinDetail(errorDetail, writeDetail ?? "sidecar write failed");
1805
+ }
1806
+
1807
+ let errorBytes = 0;
1808
+ if (errorDetail) {
1809
+ try {
1810
+ writePrivate(errorPath, `${errorDetail}\n`, deps);
1811
+ errorBytes = Buffer.byteLength(`${errorDetail}\n`, "utf8");
1812
+ } catch {
1813
+ writeFailed = true;
1814
+ errorCode = errorCode ?? "write_failed";
1815
+ }
1816
+ }
1817
+
1818
+ return closePublicResult(buildTransportResult({
1819
+ request,
1820
+ route,
1821
+ effort,
1822
+ clamped,
1823
+ authStatus,
1824
+ started,
1825
+ latencyMs,
1826
+ collected,
1827
+ fields,
1828
+ timedOut,
1829
+ killRequest,
1830
+ outputDir,
1831
+ dispatchPath,
1832
+ cliId,
1833
+ stage: spawnFailed ? "spawn" : "run",
1834
+ errorCode,
1835
+ errorDetail,
1836
+ writeFailed,
1837
+ modelClaim,
1838
+ answerPath,
1839
+ answerBytes,
1840
+ stderrPath,
1841
+ stderrBytes,
1842
+ errorPath,
1843
+ errorBytes,
1844
+ }));
1845
+ }
1846
+
1847
+ function collectChildStdio(child, options) {
1848
+ const { timeoutMs, killGraceMs, onTimeout, requestTermination } = options;
1849
+ return new Promise((resolve) => {
1850
+ let stdout = "";
1851
+ let stderr = "";
1852
+ let settled = false;
1853
+ let observedExit;
1854
+ let gotClose = false;
1855
+ let stdoutEnded = !child.stdout;
1856
+ let stderrEnded = !child.stderr;
1857
+ let timer;
1858
+ let escalateTimer;
1859
+ let missingTimer;
1860
+ let drainTimer;
1861
+ const stdioDrained = () => stdoutEnded && stderrEnded;
1862
+ const requestKill = (signal) => {
1863
+ if (observedExit) return;
1864
+ requestTermination(signal);
1865
+ };
1866
+ const unrefHandle = (handle) => {
1867
+ try {
1868
+ handle?.unref?.();
1869
+ } catch {
1870
+ // Unref is best-effort; missing unref is not a death claim.
1871
+ }
1872
+ };
1873
+ const facts = (extra = {}) => ({
1874
+ stdout,
1875
+ stderr,
1876
+ exitCode: observedExit ? observedExit.code : null,
1877
+ signal: observedExit ? observedExit.signal ?? null : null,
1878
+ observedChildExit: Boolean(observedExit),
1879
+ outputComplete: gotClose || stdioDrained(),
1880
+ ...extra,
1881
+ });
1882
+ const onStdout = (chunk) => { stdout += chunk; };
1883
+ const onStderr = (chunk) => { stderr += chunk; };
1884
+ const onStdoutEnd = () => {
1885
+ stdoutEnded = true;
1886
+ if (observedExit && stdioDrained()) finish();
1887
+ };
1888
+ const onStderrEnd = () => {
1889
+ stderrEnded = true;
1890
+ if (observedExit && stdioDrained()) finish();
1891
+ };
1892
+ const onError = (error) => finish({ error });
1893
+ const onExit = (code, signal) => {
1894
+ observedExit = { code, signal };
1895
+ if (gotClose || stdioDrained()) {
1896
+ finish();
1897
+ return;
1898
+ }
1899
+ if (!drainTimer) {
1900
+ drainTimer = setTimeout(() => finish(), killGraceMs);
1901
+ }
1902
+ };
1903
+ const onClose = (code, signal) => {
1904
+ gotClose = true;
1905
+ stdoutEnded = true;
1906
+ stderrEnded = true;
1907
+ if (!observedExit) observedExit = { code, signal };
1908
+ finish();
1909
+ };
1910
+ const detach = () => {
1911
+ child.stdout?.off?.("data", onStdout);
1912
+ child.stdout?.off?.("end", onStdoutEnd);
1913
+ child.stderr?.off?.("data", onStderr);
1914
+ child.stderr?.off?.("end", onStderrEnd);
1915
+ child.off("error", onError);
1916
+ child.off("exit", onExit);
1917
+ child.off("close", onClose);
1918
+ };
1919
+ const releaseIncomplete = () => {
1920
+ try { child.stdout?.pause?.(); } catch { /* owned collection pause only */ }
1921
+ try { child.stderr?.pause?.(); } catch { /* owned collection pause only */ }
1922
+ unrefHandle(child.stdout);
1923
+ unrefHandle(child.stderr);
1924
+ unrefHandle(child);
1925
+ };
1926
+ const finish = (extra = {}) => {
1927
+ if (settled) return;
1928
+ settled = true;
1929
+ if (timer) clearTimeout(timer);
1930
+ if (escalateTimer) clearTimeout(escalateTimer);
1931
+ if (missingTimer) clearTimeout(missingTimer);
1932
+ if (drainTimer) clearTimeout(drainTimer);
1933
+ detach();
1934
+ if (!gotClose) releaseIncomplete();
1935
+ resolve(facts(extra));
1936
+ };
1937
+ child.stdout?.setEncoding?.("utf8");
1938
+ child.stderr?.setEncoding?.("utf8");
1939
+ child.stdout?.on?.("data", onStdout);
1940
+ child.stderr?.on?.("data", onStderr);
1941
+ child.stdout?.on?.("end", onStdoutEnd);
1942
+ child.stderr?.on?.("end", onStderrEnd);
1943
+ child.on("error", onError);
1944
+ child.on("exit", onExit);
1945
+ child.on("close", onClose);
1946
+ if (timeoutMs !== undefined) {
1947
+ timer = setTimeout(() => {
1948
+ if (observedExit) {
1949
+ finish();
1950
+ return;
1951
+ }
1952
+ onTimeout();
1953
+ requestKill("SIGTERM");
1954
+ escalateTimer = setTimeout(() => {
1955
+ if (settled) return;
1956
+ requestKill("SIGKILL");
1957
+ missingTimer = setTimeout(() => finish(), killGraceMs);
1958
+ }, killGraceMs);
1959
+ }, timeoutMs);
1960
+ }
1961
+ });
1962
+ }
1963
+
1964
+ function buildTransportResult(input) {
1965
+ const {
1966
+ request,
1967
+ route,
1968
+ effort,
1969
+ clamped,
1970
+ authStatus,
1971
+ started,
1972
+ latencyMs,
1973
+ collected,
1974
+ fields,
1975
+ timedOut,
1976
+ killRequest,
1977
+ dispatchPath,
1978
+ cliId,
1979
+ stage,
1980
+ errorCode,
1981
+ writeFailed,
1982
+ modelClaim,
1983
+ answerPath,
1984
+ answerBytes,
1985
+ stderrPath,
1986
+ stderrBytes,
1987
+ errorPath,
1988
+ errorBytes,
1989
+ } = input;
1990
+ const emptyPayload = fields.emptyPayload === true;
1991
+ const spawnFailed = stage === "spawn";
1992
+ const signaled = Boolean(collected.signal) && collected.observedChildExit === true;
1993
+ const successfulExit = request.harness === "agy"
1994
+ ? collected.observedChildExit === true
1995
+ : collected.observedChildExit === true && collected.exitCode === 0;
1996
+ const outputComplete = collected.outputComplete === true;
1997
+ const status = timedOut || killRequest || signaled
1998
+ ? "interrupted"
1999
+ : (errorCode || emptyPayload || writeFailed || !successfulExit || !outputComplete || spawnFailed)
2000
+ ? "failed"
2001
+ : "completed";
2002
+ const ok = status === "completed";
2003
+ const { capped, metadata } = applyRoutingCap(fields);
2004
+ const usagePresent = [
2005
+ capped.tokensIn, capped.tokensOut, capped.cacheReadTokens, capped.cacheCreationTokens,
2006
+ capped.reasoningTokens, capped.totalTokens, fields.costUsd, fields.providerReportedCostUsd, metadata.tokensIn,
2007
+ ].some((value) => value !== undefined);
2008
+ const usagePartial = status !== "completed" && usagePresent;
2009
+ const publicErrorCode = status === "completed" ? undefined : errorCode;
2010
+ return {
2011
+ schema: RESULT_SCHEMA,
2012
+ ok,
2013
+ status,
2014
+ stage,
2015
+ harness: request.harness,
2016
+ provider: request.harness === "pi" ? piProviderOf(request.model) : route.provider,
2017
+ role: request.role,
2018
+ requestedModel: request.model,
2019
+ effectiveModel: fields.effectiveModel,
2020
+ reasoningEffort: effort,
2021
+ ...(clamped ? { effortClampedFrom: clamped } : {}),
2022
+ startedAt: new Date(started).toISOString(),
2023
+ finishedAt: new Date(started + latencyMs).toISOString(),
2024
+ latencyMs,
2025
+ exitCode: collected.exitCode === undefined ? null : collected.exitCode,
2026
+ ...(collected.signal ? { signal: collected.signal } : {}),
2027
+ timedOut,
2028
+ ...(killRequest ? { killRequest } : {}),
2029
+ observedChildExit: collected.observedChildExit === true,
2030
+ tokenBasis: TOKEN_BASIS,
2031
+ contextOccupancy: CONTEXT_OCCUPANCY_UNKNOWN,
2032
+ ...capped,
2033
+ ...(closedNonnegFinite(fields.costUsd) !== undefined ? { costUsd: closedNonnegFinite(fields.costUsd) } : {}),
2034
+ costBasis: fields.costBasis,
2035
+ ...(closedNonnegFinite(fields.providerReportedCostUsd) !== undefined
2036
+ ? { providerReportedCostUsd: closedNonnegFinite(fields.providerReportedCostUsd) }
2037
+ : {}),
2038
+ ...(usagePartial ? { usagePartial: true } : {}),
2039
+ ...(typeof fields.sessionId === "string" ? { sessionId: fields.sessionId } : {}),
2040
+ ...(fields.stopReason ? { stopReason: fields.stopReason } : {}),
2041
+ ...(publicErrorCode ? { errorCode: publicErrorCode } : {}),
2042
+ ...(fields.auxiliaryUsage?.length ? { auxiliaryUsage: fields.auxiliaryUsage } : {}),
2043
+ ...(fields.usageEvents?.length ? { usageEvents: fields.usageEvents } : {}),
2044
+ ...(fields.candidateModels && fields.effectiveModel === "unknown"
2045
+ ? { candidateModels: fields.candidateModels }
2046
+ : {}),
2047
+ auth: authStatus,
2048
+ command: cliId,
2049
+ dispatchPath,
2050
+ ...(answerBytes > 0 ? { answerPath, answerBytes } : {}),
2051
+ ...(stderrBytes > 0 ? { stderrPath, stderrBytes } : {}),
2052
+ ...(errorBytes > 0 ? { errorPath, errorBytes } : {}),
2053
+ ...(modelClaim ? { modelClaim } : {}),
2054
+ ...(Object.keys(metadata).length > 0 ? { providerMetadata: metadata } : {}),
2055
+ };
2056
+ }
2057
+
2058
+ export async function main(argv = process.argv, io = { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr }) {
2059
+ const source = argv.slice(2).find((arg) => arg !== "--");
2060
+ const raw = source && source !== "-"
2061
+ ? readFileSync(source, "utf8")
2062
+ : readFileSync(0, "utf8");
2063
+ const request = JSON.parse(raw);
2064
+ const result = await runHarness(request);
2065
+ io.stdout.write(JSON.stringify(result, undefined, 2) + "\n");
2066
+ process.exitCode = result.ok ? 0 : 1;
2067
+ return result;
2068
+ }
2069
+
2070
+ function invokedAsMain() {
2071
+ const self = fileURLToPath(import.meta.url);
2072
+ const argv1 = process.argv[1];
2073
+ if (!argv1) return false;
2074
+ try {
2075
+ return resolvePath(argv1) === self;
2076
+ } catch {
2077
+ return false;
2078
+ }
2079
+ }
2080
+
2081
+ if (invokedAsMain()) {
2082
+ main().catch((error) => {
2083
+ const stage = TRANSPORT_STAGES.includes(error.stage) ? error.stage : "preflight";
2084
+ const failure = closePublicResult({
2085
+ schema: RESULT_SCHEMA,
2086
+ ok: false,
2087
+ status: "failed",
2088
+ stage,
2089
+ errorCode: stage === "auth" ? "auth_failed" : stage === "spawn" ? "spawn_failed" : "preflight_failed",
2090
+ error: error.message,
2091
+ });
2092
+ process.stdout.write(`${JSON.stringify(failure, undefined, 2)}\n`);
2093
+ process.exit(2);
2094
+ });
2095
+ }