@kontextmind/kxm 0.6.0 → 0.7.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 (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +5 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +2 -0
  14. package/docs/agent-skills.md +118 -0
  15. package/docs/architecture.md +1 -1
  16. package/docs/assignment-runner.md +21 -8
  17. package/docs/configuration.md +2 -2
  18. package/docs/kxm-handbook.md +3 -3
  19. package/docs/operator-pi-packages.md +63 -0
  20. package/docs/skills/repo-work-delivery.md +102 -0
  21. package/docs/test-matrix.md +4 -3
  22. package/docs/troubleshooting.md +19 -0
  23. package/docs/vnext/validation.md +9 -0
  24. package/docs/webhook-workflows.md +2 -2
  25. package/examples/README.md +1 -1
  26. package/package.json +16 -17
  27. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  28. package/plugins/kxm/README.md +1 -1
  29. package/plugins/kxm/dist/cli.js +6875 -3672
  30. package/plugins/kxm/dist/core.js +214 -34
  31. package/plugins/kxm/dist/extension.js +7721 -85
  32. package/plugins/kxm/dist/mcp-server.js +75 -21
  33. package/plugins/kxm/dist/runtime.js +5403 -1014
  34. package/plugins/kxm/dist/server.js +3008 -2268
  35. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
  36. package/plugins/kxm/package.json +1 -1
  37. package/plugins/kxm/skills/SUITE.md +5 -0
  38. package/plugins/kxm/skills/hints.json +73 -0
  39. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  40. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  41. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  42. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  43. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  44. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +34 -0
  45. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  46. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  47. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  48. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +35 -0
  49. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  50. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  51. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  52. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  53. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  54. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  55. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  56. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  57. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  58. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  59. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  60. package/plugins/kxm/src/autocomplete.ts +9 -3
  61. package/plugins/kxm/src/cli.ts +1077 -11
  62. package/plugins/kxm/src/commands.ts +150 -8
  63. package/plugins/kxm/src/config.ts +7 -4
  64. package/plugins/kxm/src/context-packet.ts +172 -0
  65. package/plugins/kxm/src/extension.ts +36 -1
  66. package/plugins/kxm/src/external-effects.ts +356 -7
  67. package/plugins/kxm/src/hub.ts +2 -4
  68. package/plugins/kxm/src/improve.ts +72 -0
  69. package/plugins/kxm/src/mcp-server.ts +1 -1
  70. package/plugins/kxm/src/model-inventory.ts +127 -0
  71. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  72. package/plugins/kxm/src/policy-draft.mjs +565 -0
  73. package/plugins/kxm/src/price-calc.ts +17 -18
  74. package/plugins/kxm/src/prices.ts +32 -16
  75. package/plugins/kxm/src/producers.ts +71 -0
  76. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  77. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  78. package/plugins/kxm/src/role.ts +375 -0
  79. package/plugins/kxm/src/routing.ts +99 -1
  80. package/plugins/kxm/src/session-work.ts +9 -2
  81. package/plugins/kxm/src/studio-layout.ts +660 -17
  82. package/plugins/kxm/src/suggest.ts +7 -13
  83. package/plugins/kxm/src/telemetry.ts +82 -0
  84. package/plugins/kxm/src/tui.ts +140 -0
  85. package/plugins/kxm/src/vnext-config.ts +15 -110
  86. package/plugins/kxm/src/vnext-engine.ts +198 -62
  87. package/plugins/kxm/src/vnext-harness.ts +263 -81
  88. package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
  89. package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
  90. package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
  91. package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
  92. package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
  93. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  94. package/plugins/kxm/src/workflow-manager.ts +392 -0
  95. package/plugins/kxm/src/workflow-tui.ts +255 -0
  96. package/schemas/policy-draft/README.md +17 -0
  97. package/schemas/policy-draft/model.v2.schema.json +140 -0
  98. package/schemas/policy-draft/role.v2.schema.json +91 -0
  99. package/schemas/vnext/role.schema.json +76 -0
  100. package/schemas/vnext/run-event.schema.json +1 -0
  101. package/scripts/assignment-run.d.mts +1 -1
  102. package/scripts/assignment-run.mjs +44 -35
  103. package/scripts/check-generated.mjs +33 -9
  104. package/scripts/emit-codex-artifacts.mjs +255 -11
  105. package/scripts/harness-run.d.mts +12 -4
  106. package/scripts/harness-run.mjs +65 -17
  107. package/scripts/kxm-hub.mjs +6 -0
  108. package/scripts/native-critic.d.mts +5 -0
  109. package/scripts/native-critic.mjs +60 -0
  110. package/.kxm/config/README.md +0 -5
  111. package/.kxm/config/agents.json +0 -43
  112. package/.kxm/config/env.example +0 -56
  113. package/.kxm/config/update.example.yaml +0 -9
  114. package/.kxm/config/workflows/fix.json +0 -160
  115. package/.kxm/config/workflows/jira-development.json +0 -116
  116. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  117. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -1,14 +1,24 @@
1
- import { spawn } from "node:child_process";
1
+ import { join } from "node:path";
2
+ import { beginOneShotEvidence } from "./vnext-oneshot-evidence.ts";
3
+ import { vnextUserStateRoot } from "./vnext-bindings.ts";
4
+ import { defaultSpawn, type VnextOneShotProcessResult, type VnextOneShotSpawn } from "./vnext-oneshot-process.ts";
5
+ export { defaultSpawn, type VnextOneShotProcessResult, type VnextOneShotSpawn } from "./vnext-oneshot-process.ts";
2
6
  import {
3
7
  BUILTIN_HARNESSES,
4
8
  NATIVE_HARNESS_PROVIDERS,
5
- probeHarnessAssignment,
9
+ probeHarnessAssignmentAsync,
10
+ oneShotReadOnlyArgs,
11
+ type HarnessAssignmentProbeOptions,
12
+ type HarnessStatus,
6
13
  type HarnessCatalogEntry,
7
14
  type HarnessInventory,
15
+ type OneShotParsedOutput,
8
16
  } from "./vnext-harness.ts";
9
17
  import {
10
18
  calculateModelCost,
19
+ findModelPrice,
11
20
  loadPriceCatalog,
21
+ parsePriceCatalog,
12
22
  type PriceCatalog,
13
23
  } from "./prices.ts";
14
24
  import {
@@ -18,27 +28,9 @@ import {
18
28
  type VnextProducerResult,
19
29
  } from "./vnext-engine.ts";
20
30
 
21
- export interface VnextOneShotProcessResult {
22
- stdout: string;
23
- stderr: string;
24
- code: number | null;
25
- error?: Error | undefined;
26
- }
27
-
28
- export type VnextOneShotSpawn = (
29
- command: string,
30
- args: readonly string[],
31
- options: {
32
- cwd?: string | undefined;
33
- env?: NodeJS.ProcessEnv | undefined;
34
- input?: string | undefined;
35
- timeoutMs?: number | undefined;
36
- signal?: AbortSignal | undefined;
37
- },
38
- ) => Promise<VnextOneShotProcessResult>;
39
-
40
31
  export interface VnextOneShotProducerOptions {
41
32
  projectRoot?: string | undefined;
33
+ evidenceRoot?: string | undefined;
42
34
  defaultHarness?: string | undefined;
43
35
  defaultModel?: string | undefined;
44
36
  defaultProvider?: string | undefined;
@@ -48,7 +40,7 @@ export interface VnextOneShotProducerOptions {
48
40
  env?: NodeJS.ProcessEnv | undefined;
49
41
  timeoutMs?: number | undefined;
50
42
  spawnProcess?: VnextOneShotSpawn | undefined;
51
- probeHarness?: typeof probeHarnessAssignment | undefined;
43
+ probeHarness?: ((options: Omit<HarnessAssignmentProbeOptions, "runCommand"> & { signal?: AbortSignal | undefined }) => HarnessStatus | Promise<HarnessStatus>) | undefined;
52
44
  resolveHarness?: ((agentId: string, runId: string) => string | undefined) | undefined;
53
45
  resolveModel?: ((agentId: string, runId: string) => {
54
46
  harness?: string | undefined;
@@ -65,96 +57,20 @@ export interface VnextOneShotProducer extends VnextProducer {
65
57
  }
66
58
 
67
59
  function determineOutcome(text: string, allowedOutcomes: readonly string[]): string {
68
- const normalized = text.trim();
69
- const jsonMatch = /"outcome"\s*:\s*"([^"]+)"/.exec(normalized);
70
- if (jsonMatch && allowedOutcomes.includes(jsonMatch[1]!)) {
71
- return jsonMatch[1]!;
72
- }
73
- for (const outcome of allowedOutcomes) {
74
- const regex = new RegExp(`\\b${outcome}\\b`, "i");
75
- if (regex.test(normalized)) {
76
- return outcome;
60
+ try {
61
+ const result: unknown = JSON.parse(text.trim());
62
+ if (result && typeof result === "object" && !Array.isArray(result)) {
63
+ const outcome = (result as Record<string, unknown>).outcome;
64
+ if (typeof outcome === "string" && allowedOutcomes.includes(outcome)) return outcome;
77
65
  }
78
- }
79
- if (allowedOutcomes.includes("passed")) return "passed";
80
- return allowedOutcomes[0] ?? "completed";
81
- }
82
-
83
- export function defaultSpawn(
84
- command: string,
85
- args: readonly string[],
86
- options: {
87
- cwd?: string | undefined;
88
- env?: NodeJS.ProcessEnv | undefined;
89
- input?: string | undefined;
90
- timeoutMs?: number | undefined;
91
- signal?: AbortSignal | undefined;
92
- },
93
- ): Promise<VnextOneShotProcessResult> {
94
- return new Promise((resolve) => {
95
- let stdout = "";
96
- let stderr = "";
97
- let killed = false;
98
-
99
- const child = spawn(command, [...args], {
100
- cwd: options.cwd,
101
- env: options.env ?? process.env,
102
- stdio: ["pipe", "pipe", "pipe"],
103
- windowsHide: true,
104
- });
105
-
106
- if (options.signal) {
107
- if (options.signal.aborted) {
108
- child.kill();
109
- return resolve({ stdout: "", stderr: "aborted", code: null, error: new Error("process_aborted") });
110
- }
111
- const onAbort = () => {
112
- killed = true;
113
- child.kill();
114
- };
115
- options.signal.addEventListener("abort", onAbort, { once: true });
116
- child.on("close", () => options.signal?.removeEventListener("abort", onAbort));
117
- }
118
-
119
- if (options.timeoutMs && options.timeoutMs > 0) {
120
- const timer = setTimeout(() => {
121
- killed = true;
122
- child.kill();
123
- }, options.timeoutMs);
124
- child.on("close", () => clearTimeout(timer));
125
- }
126
-
127
- child.stdout.on("data", (chunk: Buffer | string) => {
128
- stdout += chunk.toString();
129
- });
130
- child.stderr.on("data", (chunk: Buffer | string) => {
131
- stderr += chunk.toString();
132
- });
133
-
134
- child.on("error", (err) => {
135
- resolve({ stdout, stderr, code: null, error: err });
136
- });
137
-
138
- child.on("close", (code) => {
139
- resolve({
140
- stdout,
141
- stderr,
142
- code,
143
- ...(killed ? { error: new Error("process_aborted") } : {}),
144
- });
145
- });
146
-
147
- if (options.input !== undefined) {
148
- child.stdin.write(options.input);
149
- child.stdin.end();
150
- } else {
151
- child.stdin.end();
152
- }
153
- });
66
+ } catch { /* Prose, substring matches, and missing results cannot imply success. */ }
67
+ return "failed";
154
68
  }
155
69
 
156
70
  export function createVnextOneShotProducer(options: VnextOneShotProducerOptions = {}): VnextOneShotProducer {
157
71
  const defaultHarness = options.defaultHarness ?? "claude";
72
+ const running = new Map<AbortController, Promise<VnextProducerResult>>();
73
+ let closed = false;
158
74
 
159
75
  function resolveHarnessForRequest(request: VnextProducerRequest): string {
160
76
  if (request.harness) return request.harness;
@@ -202,12 +118,14 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
202
118
  };
203
119
  }
204
120
  }
205
- const defaultModel = options.defaultModel ?? (harness === "codex" ? "gpt-5.6-sol" : "claude-3-7-sonnet");
121
+ const defaultModel = options.defaultModel ?? (
122
+ harness === "codex" ? "gpt-5.6-sol" : harness === "kimi" ? "kimi-for-coding" : harness === "agy" ? "gemini-3.8-flash-high" : "claude-3-7-sonnet"
123
+ );
206
124
  const parsed = parseModelString(defaultModel, harness);
207
125
  return { provider: parsed.provider, model: parsed.model, thinking: request.thinking };
208
126
  }
209
127
 
210
- function checkAuth(harness: string, provider: string, model: string): void {
128
+ async function checkAuth(harness: string, provider: string, model: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<HarnessStatus> {
211
129
  if (options.inventory) {
212
130
  const entry = options.inventory.harnesses.find((h) => h.id === harness);
213
131
  if (!entry || !entry.detected || entry.authenticated !== true) {
@@ -215,13 +133,8 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
215
133
  }
216
134
  }
217
135
 
218
- const probeFn = options.probeHarness ?? probeHarnessAssignment;
219
- const probe = probeFn({
220
- harness,
221
- provider,
222
- model,
223
- env: options.env,
224
- });
136
+ const probeFn = options.probeHarness ?? probeHarnessAssignmentAsync;
137
+ const probe = await probeFn({ harness, provider, model, env, signal, timeoutMs: 10_000 });
225
138
 
226
139
  if (!probe.detected) {
227
140
  throw new Error(`${harness}_not_authenticated: ${harness} harness not detected (${probe.issues.join(", ")})`);
@@ -229,26 +142,32 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
229
142
  if (probe.authenticated !== true) {
230
143
  throw new Error(`${harness}_not_authenticated: ${harness} not authenticated (${probe.issues.join(", ")})`);
231
144
  }
145
+ return probe;
232
146
  }
233
147
 
234
- const producer: VnextOneShotProducer = {
235
- id: "oneshot",
236
-
237
- async produce(request: VnextProducerRequest): Promise<VnextProducerResult> {
148
+ async function executeRequest(request: VnextProducerRequest): Promise<VnextProducerResult> {
238
149
  const startTime = Date.now();
239
150
  const harness = resolveHarnessForRequest(request);
240
151
  const resolved = resolveModelForRequest(request, harness);
241
152
 
242
- // Preflight fail-closed check
243
- checkAuth(harness, resolved.provider, resolved.model);
244
-
153
+ if (closed) throw new Error("oneshot_producer_closed");
154
+ const cancelled = (): VnextProducerResult => ({ outcome: "cancelled", costBasis: "unknown", costUsd: null,
155
+ latencyMs: Math.max(0, Date.now() - startTime), harness, provider: resolved.provider,
156
+ requestedModel: resolved.model, effectiveModel: "unknown", agentRole: request.agentRole ?? request.agentId });
157
+ if (request.signal.aborted) return cancelled();
245
158
  const catalogEntry = (options.catalog ?? BUILTIN_HARNESSES).find((h) => h.id === harness);
246
- if (!catalogEntry || !catalogEntry.oneShot) {
247
- const hint = harness === "gemini" ? " (Gemini CLI is deprecated; use agy for Google models)" : "";
248
- throw new Error(`oneshot_harness_unsupported: ${harness}${hint}`);
249
- }
250
-
251
- const command = catalogEntry.commands[0] ?? harness;
159
+ if (!catalogEntry?.oneShot) throw new Error(`oneshot_harness_unsupported: ${harness}`);
160
+ const permissionArgs = oneShotReadOnlyArgs(harness);
161
+ if (!permissionArgs) throw new Error(`oneshot_harness_unsupported: ${harness} permission_profile_unaudited`);
162
+ // Pin the environment for auth and execution; don't observe subscription
163
+ // auth under one environment and then spawn under changed API-key settings.
164
+ const env = { ...(options.env ?? process.env) };
165
+ let auth: HarnessStatus;
166
+ try { auth = await checkAuth(harness, resolved.provider, resolved.model, env, request.signal); }
167
+ catch (error) { if (request.signal.aborted) return cancelled(); throw error; }
168
+ if (request.signal.aborted) return cancelled();
169
+
170
+ const command = auth.command ?? catalogEntry.commands[0] ?? harness;
252
171
  const oneShot = catalogEntry.oneShot;
253
172
 
254
173
  let args: string[];
@@ -258,6 +177,7 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
258
177
  "--model",
259
178
  resolved.model,
260
179
  ...(resolved.thinking ? ["--effort", resolved.thinking] : []),
180
+ ...permissionArgs,
261
181
  "--output-format",
262
182
  "json",
263
183
  ];
@@ -267,134 +187,129 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
267
187
  "-m",
268
188
  resolved.model,
269
189
  ...(resolved.thinking ? ["-c", `model_reasoning_effort="${resolved.thinking}"`] : []),
190
+ ...permissionArgs,
270
191
  "--json",
271
192
  "-",
272
193
  ];
273
- } else if (harness === "kimi") {
274
- args = [
275
- "--output-format",
276
- "stream-json",
277
- "-m",
278
- resolved.model,
279
- "-p",
280
- ];
281
- } else if (harness === "agy") {
282
- args = [
283
- "--output-format",
284
- "json",
285
- ...(resolved.thinking ? ["--effort", resolved.thinking] : []),
286
- "--model",
287
- resolved.model,
288
- "-p",
289
- ];
194
+ } else if (harness === "grok") {
195
+ args = ["--model", resolved.model,
196
+ ...(resolved.thinking ? ["--reasoning-effort", resolved.thinking] : []),
197
+ ...permissionArgs, "--output-format", "json"];
290
198
  } else {
291
- args = [
292
- ...oneShot.argv,
293
- "--model",
294
- resolved.model,
295
- ...(resolved.thinking ? ["--thinking", resolved.thinking] : []),
296
- ];
199
+ throw new Error(`oneshot_harness_unsupported: ${harness} permission_profile_unaudited`);
297
200
  }
298
201
 
299
- const promptMessage = request.prompt
300
- ?? `Execute step ${request.stepId} (attempt ${request.stepAttempt}) for agent ${request.agentId}. Allowed outcomes: ${request.allowedOutcomes.join(", ")}.`;
202
+ const taskPrompt = request.prompt
203
+ ?? `Execute step ${request.stepId} (attempt ${request.stepAttempt}) for agent ${request.agentId}.`;
204
+ const promptMessage = `${taskPrompt}\n\nReturn a final JSON object with an "outcome" field chosen from ${JSON.stringify(request.allowedOutcomes)} and a concise "summary". Do not wrap it in Markdown. If no allowed outcome is truthful, return {"outcome":"failed"}; never infer success from an incomplete task.`;
301
205
 
302
206
  let input: string | undefined;
303
207
  if (oneShot.promptVia === "stdin") {
304
208
  input = promptMessage;
305
209
  } else {
210
+ if (harness === "grok") args.push("--single");
306
211
  args.push(promptMessage);
307
212
  }
308
213
 
309
- if (request.signal?.aborted) {
310
- const outcome = request.allowedOutcomes.includes("cancelled")
311
- ? "cancelled"
312
- : (request.allowedOutcomes.includes("failed") ? "failed" : request.allowedOutcomes[0]!);
313
- return {
314
- outcome,
315
- costBasis: "unknown",
316
- costUsd: null,
317
- latencyMs: 0,
318
- harness,
319
- provider: resolved.provider,
320
- requestedModel: resolved.model,
321
- effectiveModel: resolved.model,
322
- agentRole: request.agentRole ?? request.agentId,
323
- };
324
- }
325
-
326
214
  const spawnFn = options.spawnProcess ?? defaultSpawn;
327
- const procResult = await spawnFn(command, args, {
328
- cwd: options.projectRoot ?? process.cwd(),
329
- env: options.env,
330
- input,
331
- timeoutMs: options.timeoutMs,
332
- signal: request.signal,
333
- });
334
-
335
- if (request.signal?.aborted || procResult.error?.message === "process_aborted") {
336
- const outcome = request.allowedOutcomes.includes("cancelled")
337
- ? "cancelled"
338
- : (request.allowedOutcomes.includes("failed") ? "failed" : request.allowedOutcomes[0]!);
339
- return {
340
- outcome,
341
- costBasis: "unknown",
342
- costUsd: null,
343
- latencyMs: Math.max(0, Date.now() - startTime),
344
- harness,
345
- provider: resolved.provider,
346
- requestedModel: resolved.model,
347
- effectiveModel: resolved.model,
348
- agentRole: request.agentRole ?? request.agentId,
349
- };
215
+ const cwd = options.projectRoot ?? process.cwd();
216
+ const evidence = await beginOneShotEvidence(options.evidenceRoot ?? join(vnextUserStateRoot(), "runtime", "oneshot-evidence"), {
217
+ runId: request.runId, stepId: request.stepId, attemptId: request.attemptId, assignmentId: request.assignmentId,
218
+ harness, provider: resolved.provider, model: resolved.model, cwd, command, args, input,
219
+ }, [request.capability, ...Object.entries(env).filter(([key]) => /TOKEN|KEY|SECRET|PASSWORD|COOKIE|AUTH/i.test(key)).map(([, value]) => value ?? "")]);
220
+ let procResult: VnextOneShotProcessResult;
221
+ try {
222
+ procResult = request.signal.aborted
223
+ ? { stdout: "", stderr: "", code: null, started: false, observedChildExit: false, error: new Error("process_aborted") }
224
+ : await spawnFn(command, args, { cwd, env, input, timeoutMs: options.timeoutMs ?? 120_000, signal: request.signal });
225
+ } catch (error) {
226
+ // A rejected adapter promise is not proof that no process/effect started.
227
+ procResult = { stdout: "", stderr: "", code: null, observedChildExit: false, error: error instanceof Error ? error : new Error("process_adapter_error") };
350
228
  }
351
229
 
352
- const parsed = oneShot.usageParser(procResult.stdout, procResult.stderr);
353
- let outcome: string;
354
- if (parsed.isError) {
355
- outcome = request.allowedOutcomes.includes("failed") ? "failed" : request.allowedOutcomes[0]!;
356
- } else {
357
- outcome = determineOutcome(parsed.text || procResult.stdout, request.allowedOutcomes);
230
+ // Parse available usage even when execution failed or was interrupted.
231
+ let parsed: OneShotParsedOutput;
232
+ try { parsed = oneShot.usageParser(procResult.stdout, procResult.stderr, resolved.model); }
233
+ catch { parsed = { text: "", isError: true }; }
234
+ const aborted = request.signal.aborted || procResult.error?.message === "process_aborted";
235
+ // Process groups are cleanup, not containment. A detached descendant or
236
+ // provider leader may outlive the client; interrupted effects need recovery.
237
+ const effectUncertain = procResult.terminationRequested === true || Boolean(procResult.signal)
238
+ || procResult.error?.message === "process_exit_unobserved"
239
+ || (procResult.observedChildExit === false && procResult.started !== false);
240
+ const transportFailed = procResult.code !== 0 || Boolean(procResult.error) || effectUncertain;
241
+ const outcome = aborted ? "cancelled" : transportFailed || parsed.isError ? "failed"
242
+ : determineOutcome(parsed.text, request.allowedOutcomes);
243
+ const providerMetadata: Record<string, string | number | boolean> = {
244
+ processStatus: aborted ? "aborted" : transportFailed ? "failed" : "completed",
245
+ executionEvidenceId: evidence.id,
246
+ };
247
+ if (procResult.code !== null && Number.isFinite(procResult.code)) providerMetadata.processExitCode = procResult.code;
248
+ if (procResult.signal) providerMetadata.processSignal = procResult.signal;
249
+ if (procResult.observedChildExit !== undefined) providerMetadata.observedChildExit = procResult.observedChildExit;
250
+ if (procResult.started !== undefined) providerMetadata.processStarted = procResult.started;
251
+ if (procResult.terminationRequested !== undefined) providerMetadata.terminationRequested = procResult.terminationRequested;
252
+ if (procResult.error) {
253
+ const reason = procResult.error.message;
254
+ providerMetadata.processError = /^process_(aborted|timeout|output_limit|stdin_error|stdio_error|stdio_unclosed|exit_unobserved)$/.test(reason) ? reason : "process_error";
358
255
  }
359
-
360
256
  const latencyMs = Math.max(0, Date.now() - startTime);
361
257
  const usage = parsed.usage;
362
- const tokensIn = typeof usage?.tokensIn === "number" ? usage.tokensIn : null;
363
- const tokensOut = typeof usage?.tokensOut === "number" ? usage.tokensOut : null;
364
- const cacheReadTokens = typeof usage?.cacheReadTokens === "number" ? usage.cacheReadTokens : null;
365
- const cacheWriteTokens = typeof usage?.cacheWriteTokens === "number" ? usage.cacheWriteTokens : null;
366
- const contextTokens = typeof usage?.contextTokens === "number" ? usage.contextTokens : tokensIn;
367
-
368
- // Price calculation: metered when price row exists, else unmetered for subscription with priceRef
369
- const catalog = options.priceCatalog ?? loadPriceCatalog(options.projectRoot ?? process.cwd());
370
- let costBasis: "metered" | "unmetered" | "unknown" = "unknown";
371
- let costUsd: number | null = null;
372
- let priceRef: string | undefined;
373
-
374
- if (catalog) {
258
+ const validCount = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
259
+ const boundedCount = (field: string, value: unknown): number | null => {
260
+ if (validCount(value) && value <= 1_000_000) return value;
261
+ if (typeof value === "number" && Number.isFinite(value)) {
262
+ providerMetadata[`raw${field[0]!.toUpperCase()}${field.slice(1)}`] = value;
263
+ }
264
+ return null;
265
+ };
266
+ const tokensIn = boundedCount("tokensIn", usage?.tokensIn);
267
+ const tokensOut = boundedCount("tokensOut", usage?.tokensOut);
268
+ const cacheReadTokens = boundedCount("cacheReadTokens", usage?.cacheReadTokens);
269
+ const cacheWriteTokens = boundedCount("cacheWriteTokens", usage?.cacheWriteTokens);
270
+ const contextTokens = boundedCount("contextTokens", usage?.contextTokens);
271
+ if (typeof usage?.costUsd === "number" && Number.isFinite(usage.costUsd) && usage.costUsd >= 0) providerMetadata.providerReportedCostUsd = usage.costUsd;
272
+
273
+ // List estimates and provider-reported amounts are not proof of a bill.
274
+ let catalog: PriceCatalog | undefined;
275
+ try {
276
+ catalog = options.priceCatalog ? parsePriceCatalog(JSON.stringify(options.priceCatalog))
277
+ : loadPriceCatalog(options.projectRoot ?? process.cwd());
278
+ } catch { providerMetadata.priceCatalogUnavailable = true; }
279
+ // Without a freshness-aware vendor feed, don't silently quote an older
280
+ // local snapshot. Even today's hash-verified snapshot is only an estimate.
281
+ if (catalog && catalog.date !== new Date().toISOString().slice(0, 10)) {
282
+ providerMetadata.priceCatalogStale = true;
283
+ catalog = undefined;
284
+ }
285
+ const subscription = ["claude.ai", "ChatGPT", "antigravity-oauth"].includes(auth.authMethod ?? "");
286
+ const costBasis = subscription ? "unmetered" as const : "unknown" as const;
287
+ const costUsd = null;
288
+ const priceRef = subscription ? `subscription:${harness}` : undefined;
289
+ if (auth.authMethod) providerMetadata.authMethod = auth.authMethod;
290
+ const priceRow = catalog ? findModelPrice(catalog, resolved.model, resolved.provider) : undefined;
291
+ const flatTier = priceRow?.tiers.length === 1 && priceRow.tiers[0]?.upToContextTokens == null ? priceRow.tiers[0] : undefined;
292
+ // Unknown context occupancy cannot select a context tier. Claude reports
293
+ // uncached input separately; other protocols need their own price semantics.
294
+ if (catalog && harness === "claude" && flatTier
295
+ && validCount(usage?.tokensIn) && validCount(usage?.tokensOut)
296
+ && validCount(usage?.cacheReadTokens) && validCount(usage?.cacheWriteTokens)
297
+ && (usage.cacheReadTokens === 0 || flatTier.cacheReadPerMillion != null)
298
+ && (usage.cacheWriteTokens === 0 || flatTier.cacheWritePerMillion != null)) {
375
299
  const calculated = calculateModelCost(catalog, {
376
- model: resolved.model,
377
- provider: resolved.provider,
378
- tokensIn,
379
- tokensOut,
380
- cacheReadTokens,
381
- cacheWriteTokens,
300
+ model: resolved.model, provider: resolved.provider,
301
+ tokensIn: usage.tokensIn, tokensOut: usage.tokensOut,
302
+ cacheReadTokens: usage.cacheReadTokens, cacheWriteTokens: usage.cacheWriteTokens,
382
303
  contextTokens,
383
304
  });
384
- if (calculated) {
385
- costBasis = "metered";
386
- costUsd = calculated.costUsd;
387
- priceRef = calculated.priceRef;
305
+ if (calculated && Number.isFinite(calculated.costUsd)) {
306
+ providerMetadata.listCostUsd = calculated.costUsd;
307
+ providerMetadata.listPriceRef = calculated.priceRef;
308
+ providerMetadata.listPriceSha256 = catalog.sha256;
388
309
  }
389
310
  }
390
311
 
391
- if (costBasis === "unknown" && (harness === "claude" || harness === "codex" || harness === "agy")) {
392
- costBasis = "unmetered";
393
- costUsd = null;
394
- priceRef = `subscription:${harness}`;
395
- }
396
-
397
- return {
312
+ const result: VnextProducerResult = {
398
313
  outcome,
399
314
  costBasis,
400
315
  costUsd,
@@ -407,15 +322,37 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
407
322
  harness,
408
323
  provider: resolved.provider,
409
324
  requestedModel: resolved.model,
410
- effectiveModel: resolved.model,
325
+ effectiveModel: parsed.effectiveModel ?? "unknown",
326
+ providerMetadata,
327
+ ...(effectUncertain ? { effectUncertain: true as const } : {}),
411
328
  ...(resolved.thinking !== undefined ? { thinking: resolved.thinking } : {}),
412
329
  agentRole: request.agentRole ?? request.agentId,
413
330
  ...(priceRef !== undefined ? { priceRef } : {}),
414
331
  };
332
+ try { providerMetadata.executionEvidenceSha256 = await evidence.finish(procResult, result); }
333
+ catch {
334
+ providerMetadata.evidenceWriteFailed = true;
335
+ return { ...result, outcome: "failed", effectUncertain: true };
336
+ }
337
+ return result;
338
+ }
339
+
340
+ const producer: VnextOneShotProducer = {
341
+ id: "oneshot",
342
+ produce(request): Promise<VnextProducerResult> {
343
+ const controller = new AbortController();
344
+ const signal = AbortSignal.any([request.signal, controller.signal]);
345
+ const pending = Promise.resolve().then(() => executeRequest({ ...request, signal }))
346
+ .finally(() => { running.delete(controller); });
347
+ running.set(controller, pending);
348
+ return pending;
415
349
  },
416
350
 
417
351
  async close(): Promise<void> {
418
- // One-shot producer holds no persistent process pools
352
+ closed = true;
353
+ const pending = [...running.values()];
354
+ for (const controller of running.keys()) controller.abort();
355
+ await Promise.allSettled(pending);
419
356
  },
420
357
  };
421
358
 
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { existsSync, lstatSync, mkdirSync, realpathSync } from "node:fs";
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import { VnextConfigError, validateRunEvent, vnextCanonicalJson, type JsonValue, type VnextConfigIssue, type VnextConfigOptions } from "./vnext-config.ts";
@@ -658,6 +658,40 @@ export class VnextRunEventStore {
658
658
  this.database.close();
659
659
  }
660
660
 
661
+ /** Persist the accepted prompt outside the event log (hashed in events only). */
662
+ putRunPrompt(runId: string, prompt: string): void {
663
+ const all = this.readRunPrompts();
664
+ all[runId] = prompt;
665
+ this.writeRunPrompts(all);
666
+ }
667
+
668
+ getRunPrompt(runId: string): string | undefined {
669
+ const value = this.readRunPrompts()[runId];
670
+ return typeof value === "string" ? value : undefined;
671
+ }
672
+
673
+ private runPromptSidecarPath(): string {
674
+ return `${this.path}.run-prompts.json`;
675
+ }
676
+
677
+ private readRunPrompts(): Record<string, string> {
678
+ const sidecar = this.runPromptSidecarPath();
679
+ if (!existsSync(sidecar)) return {};
680
+ try {
681
+ const parsed = JSON.parse(readFileSync(sidecar, "utf8")) as unknown;
682
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
683
+ return Object.fromEntries(
684
+ Object.entries(parsed).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
685
+ );
686
+ } catch {
687
+ return {};
688
+ }
689
+ }
690
+
691
+ private writeRunPrompts(all: Record<string, string>): void {
692
+ writeFileSync(this.runPromptSidecarPath(), `${JSON.stringify(all)}\n`, { encoding: "utf8", mode: 0o600 });
693
+ }
694
+
661
695
  /** Run one immutable transaction, rolling back on any failure. */
662
696
  transaction<T>(work: () => T): T {
663
697
  return withDatabaseTransaction(this.database, work);