@kontextmind/kxm 0.6.0 → 0.7.4

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 (136) 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 +7 -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 +4 -0
  14. package/docs/agent-skills.md +121 -0
  15. package/docs/architecture.md +1 -1
  16. package/docs/assignment-runner.md +21 -8
  17. package/docs/configuration.md +11 -2
  18. package/docs/getting-started.md +21 -0
  19. package/docs/kxm-handbook.md +3 -3
  20. package/docs/operations.md +24 -0
  21. package/docs/operator-pi-packages.md +63 -0
  22. package/docs/skills/repo-work-delivery.md +107 -0
  23. package/docs/skills.md +2 -0
  24. package/docs/test-matrix.md +4 -3
  25. package/docs/troubleshooting.md +41 -1
  26. package/docs/vnext/validation.md +9 -0
  27. package/docs/webhook-workflows.md +2 -2
  28. package/examples/README.md +1 -1
  29. package/package.json +16 -17
  30. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  31. package/plugins/kxm/README.md +1 -1
  32. package/plugins/kxm/dist/cli.js +8955 -3934
  33. package/plugins/kxm/dist/core.js +271 -34
  34. package/plugins/kxm/dist/extension.js +7759 -86
  35. package/plugins/kxm/dist/mcp-server.js +75 -21
  36. package/plugins/kxm/dist/runtime.js +5637 -1125
  37. package/plugins/kxm/dist/server.js +3125 -2260
  38. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5808 -565
  39. package/plugins/kxm/package.json +1 -1
  40. package/plugins/kxm/skills/SUITE.md +5 -0
  41. package/plugins/kxm/skills/hints.json +73 -0
  42. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  43. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  44. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  45. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  46. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  47. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  48. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  49. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  50. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  51. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  52. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  53. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  54. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  55. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  56. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  57. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  58. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  59. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  60. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  61. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  62. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  63. package/plugins/kxm/src/autocomplete.ts +9 -3
  64. package/plugins/kxm/src/cli.ts +1637 -73
  65. package/plugins/kxm/src/commands.ts +150 -8
  66. package/plugins/kxm/src/completion-install.ts +223 -0
  67. package/plugins/kxm/src/config.ts +7 -4
  68. package/plugins/kxm/src/context-packet.ts +172 -0
  69. package/plugins/kxm/src/database.ts +1 -1
  70. package/plugins/kxm/src/extension.ts +36 -1
  71. package/plugins/kxm/src/external-effects.ts +357 -8
  72. package/plugins/kxm/src/hub-env.ts +193 -0
  73. package/plugins/kxm/src/hub.ts +2 -4
  74. package/plugins/kxm/src/improve.ts +72 -0
  75. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  76. package/plugins/kxm/src/local-snapshot.ts +1 -1
  77. package/plugins/kxm/src/mcp-server.ts +1 -1
  78. package/plugins/kxm/src/model-inventory.ts +127 -0
  79. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  80. package/plugins/kxm/src/policy-draft.mjs +565 -0
  81. package/plugins/kxm/src/price-calc.ts +17 -18
  82. package/plugins/kxm/src/prices.ts +32 -16
  83. package/plugins/kxm/src/producers.ts +71 -0
  84. package/plugins/kxm/src/protocol.ts +111 -0
  85. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  86. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  87. package/plugins/kxm/src/role.ts +710 -0
  88. package/plugins/kxm/src/routing.ts +99 -1
  89. package/plugins/kxm/src/safety-integrity.ts +76 -0
  90. package/plugins/kxm/src/session-work.ts +9 -2
  91. package/plugins/kxm/src/sqlite.ts +76 -0
  92. package/plugins/kxm/src/store.ts +1 -1
  93. package/plugins/kxm/src/studio-layout.ts +660 -17
  94. package/plugins/kxm/src/suggest.ts +7 -13
  95. package/plugins/kxm/src/telemetry.ts +82 -0
  96. package/plugins/kxm/src/tui.ts +140 -0
  97. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  98. package/plugins/kxm/src/vnext-config.ts +53 -111
  99. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  100. package/plugins/kxm/src/vnext-engine.ts +214 -62
  101. package/plugins/kxm/src/vnext-harness.ts +267 -83
  102. package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
  103. package/plugins/kxm/src/vnext-oneshot-process.ts +166 -0
  104. package/plugins/kxm/src/vnext-oneshot-producer.ts +179 -224
  105. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  106. package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
  107. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  108. package/plugins/kxm/src/workflow-manager.ts +392 -0
  109. package/plugins/kxm/src/workflow-tui.ts +255 -0
  110. package/plugins/kxm/src/workflow.ts +144 -0
  111. package/schemas/policy-draft/README.md +17 -0
  112. package/schemas/policy-draft/model.v2.schema.json +140 -0
  113. package/schemas/policy-draft/role.v2.schema.json +91 -0
  114. package/schemas/vnext/role.schema.json +76 -0
  115. package/schemas/vnext/run-event.schema.json +1 -0
  116. package/scripts/assignment-run.d.mts +1 -1
  117. package/scripts/assignment-run.mjs +44 -35
  118. package/scripts/check-generated.mjs +33 -9
  119. package/scripts/emit-codex-artifacts.mjs +255 -11
  120. package/scripts/harness-run.d.mts +12 -4
  121. package/scripts/harness-run.mjs +65 -17
  122. package/scripts/kxm-bump-version.mjs +146 -0
  123. package/scripts/kxm-hub.mjs +150 -2
  124. package/scripts/kxm-publish-npm.mjs +3 -1
  125. package/scripts/kxm-release-github.mjs +3 -1
  126. package/scripts/kxm.mjs +0 -0
  127. package/scripts/native-critic.d.mts +5 -0
  128. package/scripts/native-critic.mjs +60 -0
  129. package/.kxm/config/README.md +0 -5
  130. package/.kxm/config/agents.json +0 -43
  131. package/.kxm/config/env.example +0 -56
  132. package/.kxm/config/update.example.yaml +0 -9
  133. package/.kxm/config/workflows/fix.json +0 -160
  134. package/.kxm/config/workflows/jira-development.json +0 -116
  135. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  136. 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;
77
- }
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();
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;
152
65
  }
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,147 @@ 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") {
194
+ } else if (harness === "grok") {
195
+ args = ["--model", resolved.model,
196
+ ...(resolved.thinking ? ["--reasoning-effort", resolved.thinking] : []),
197
+ ...permissionArgs, "--output-format", "json"];
198
+ } else if (harness === "agy") {
274
199
  args = [
275
- "--output-format",
276
- "stream-json",
277
200
  "-m",
278
201
  resolved.model,
279
- "-p",
280
- ];
281
- } else if (harness === "agy") {
282
- args = [
202
+ ...permissionArgs,
283
203
  "--output-format",
284
204
  "json",
285
- ...(resolved.thinking ? ["--effort", resolved.thinking] : []),
286
- "--model",
287
- resolved.model,
288
205
  "-p",
289
206
  ];
290
- } else {
207
+ } else if (harness === "kimi") {
291
208
  args = [
292
- ...oneShot.argv,
293
- "--model",
209
+ "-m",
294
210
  resolved.model,
295
- ...(resolved.thinking ? ["--thinking", resolved.thinking] : []),
211
+ ...permissionArgs,
212
+ "--output-format",
213
+ "stream-json",
214
+ "-p",
296
215
  ];
216
+ } else {
217
+ throw new Error(`oneshot_harness_unsupported: ${harness} permission_profile_unaudited`);
297
218
  }
298
219
 
299
- const promptMessage = request.prompt
300
- ?? `Execute step ${request.stepId} (attempt ${request.stepAttempt}) for agent ${request.agentId}. Allowed outcomes: ${request.allowedOutcomes.join(", ")}.`;
220
+ const taskPrompt = request.prompt
221
+ ?? `Execute step ${request.stepId} (attempt ${request.stepAttempt}) for agent ${request.agentId}.`;
222
+ 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
223
 
302
224
  let input: string | undefined;
303
225
  if (oneShot.promptVia === "stdin") {
304
226
  input = promptMessage;
305
227
  } else {
228
+ if (harness === "grok") args.push("--single");
306
229
  args.push(promptMessage);
307
230
  }
308
231
 
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
232
  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
- };
233
+ const cwd = options.projectRoot ?? process.cwd();
234
+ const evidence = await beginOneShotEvidence(options.evidenceRoot ?? join(vnextUserStateRoot(), "runtime", "oneshot-evidence"), {
235
+ runId: request.runId, stepId: request.stepId, attemptId: request.attemptId, assignmentId: request.assignmentId,
236
+ harness, provider: resolved.provider, model: resolved.model, cwd, command, args, input,
237
+ }, [request.capability, ...Object.entries(env).filter(([key]) => /TOKEN|KEY|SECRET|PASSWORD|COOKIE|AUTH/i.test(key)).map(([, value]) => value ?? "")]);
238
+ let procResult: VnextOneShotProcessResult;
239
+ try {
240
+ procResult = request.signal.aborted
241
+ ? { stdout: "", stderr: "", code: null, started: false, observedChildExit: false, error: new Error("process_aborted") }
242
+ : await spawnFn(command, args, { cwd, env, input, timeoutMs: options.timeoutMs ?? 120_000, signal: request.signal });
243
+ } catch (error) {
244
+ // A rejected adapter promise is not proof that no process/effect started.
245
+ procResult = { stdout: "", stderr: "", code: null, observedChildExit: false, error: error instanceof Error ? error : new Error("process_adapter_error") };
350
246
  }
351
247
 
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);
248
+ // Parse available usage even when execution failed or was interrupted.
249
+ let parsed: OneShotParsedOutput;
250
+ try { parsed = oneShot.usageParser(procResult.stdout, procResult.stderr, resolved.model); }
251
+ catch { parsed = { text: "", isError: true }; }
252
+ const aborted = request.signal.aborted || procResult.error?.message === "process_aborted";
253
+ // Process groups are cleanup, not containment. A detached descendant or
254
+ // provider leader may outlive the client; interrupted effects need recovery.
255
+ const effectUncertain = procResult.terminationRequested === true || Boolean(procResult.signal)
256
+ || procResult.error?.message === "process_exit_unobserved"
257
+ || (procResult.observedChildExit === false && procResult.started !== false);
258
+ const transportFailed = procResult.code !== 0 || Boolean(procResult.error) || effectUncertain;
259
+ const outcome = aborted ? "cancelled" : transportFailed || parsed.isError ? "failed"
260
+ : determineOutcome(parsed.text, request.allowedOutcomes);
261
+ const providerMetadata: Record<string, string | number | boolean> = {
262
+ processStatus: aborted ? "aborted" : transportFailed ? "failed" : "completed",
263
+ executionEvidenceId: evidence.id,
264
+ };
265
+ if (procResult.code !== null && Number.isFinite(procResult.code)) providerMetadata.processExitCode = procResult.code;
266
+ if (procResult.signal) providerMetadata.processSignal = procResult.signal;
267
+ if (procResult.observedChildExit !== undefined) providerMetadata.observedChildExit = procResult.observedChildExit;
268
+ if (procResult.started !== undefined) providerMetadata.processStarted = procResult.started;
269
+ if (procResult.terminationRequested !== undefined) providerMetadata.terminationRequested = procResult.terminationRequested;
270
+ if (procResult.error) {
271
+ const reason = procResult.error.message;
272
+ providerMetadata.processError = /^process_(aborted|timeout|output_limit|stdin_error|stdio_error|stdio_unclosed|exit_unobserved)$/.test(reason) ? reason : "process_error";
358
273
  }
359
-
360
274
  const latencyMs = Math.max(0, Date.now() - startTime);
361
275
  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) {
276
+ const validCount = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
277
+ const boundedCount = (field: string, value: unknown): number | null => {
278
+ if (validCount(value) && value <= 1_000_000) return value;
279
+ if (typeof value === "number" && Number.isFinite(value)) {
280
+ providerMetadata[`raw${field[0]!.toUpperCase()}${field.slice(1)}`] = value;
281
+ }
282
+ return null;
283
+ };
284
+ const tokensIn = boundedCount("tokensIn", usage?.tokensIn);
285
+ const tokensOut = boundedCount("tokensOut", usage?.tokensOut);
286
+ const cacheReadTokens = boundedCount("cacheReadTokens", usage?.cacheReadTokens);
287
+ const cacheWriteTokens = boundedCount("cacheWriteTokens", usage?.cacheWriteTokens);
288
+ const contextTokens = boundedCount("contextTokens", usage?.contextTokens);
289
+ if (typeof usage?.costUsd === "number" && Number.isFinite(usage.costUsd) && usage.costUsd >= 0) providerMetadata.providerReportedCostUsd = usage.costUsd;
290
+
291
+ // List estimates and provider-reported amounts are not proof of a bill.
292
+ let catalog: PriceCatalog | undefined;
293
+ try {
294
+ catalog = options.priceCatalog ? parsePriceCatalog(JSON.stringify(options.priceCatalog))
295
+ : loadPriceCatalog(options.projectRoot ?? process.cwd());
296
+ } catch { providerMetadata.priceCatalogUnavailable = true; }
297
+ // Without a freshness-aware vendor feed, don't silently quote an older
298
+ // local snapshot. Even today's hash-verified snapshot is only an estimate.
299
+ if (catalog && catalog.date !== new Date().toISOString().slice(0, 10)) {
300
+ providerMetadata.priceCatalogStale = true;
301
+ catalog = undefined;
302
+ }
303
+ const subscription = ["claude.ai", "ChatGPT", "antigravity-oauth"].includes(auth.authMethod ?? "");
304
+ const costBasis = subscription ? "unmetered" as const : "unknown" as const;
305
+ const costUsd = null;
306
+ const priceRef = subscription ? `subscription:${harness}` : undefined;
307
+ if (auth.authMethod) providerMetadata.authMethod = auth.authMethod;
308
+ const priceRow = catalog ? findModelPrice(catalog, resolved.model, resolved.provider) : undefined;
309
+ const flatTier = priceRow?.tiers.length === 1 && priceRow.tiers[0]?.upToContextTokens == null ? priceRow.tiers[0] : undefined;
310
+ // Unknown context occupancy cannot select a context tier. Claude reports
311
+ // uncached input separately; other protocols need their own price semantics.
312
+ if (catalog && harness === "claude" && flatTier
313
+ && validCount(usage?.tokensIn) && validCount(usage?.tokensOut)
314
+ && validCount(usage?.cacheReadTokens) && validCount(usage?.cacheWriteTokens)
315
+ && (usage.cacheReadTokens === 0 || flatTier.cacheReadPerMillion != null)
316
+ && (usage.cacheWriteTokens === 0 || flatTier.cacheWritePerMillion != null)) {
375
317
  const calculated = calculateModelCost(catalog, {
376
- model: resolved.model,
377
- provider: resolved.provider,
378
- tokensIn,
379
- tokensOut,
380
- cacheReadTokens,
381
- cacheWriteTokens,
318
+ model: resolved.model, provider: resolved.provider,
319
+ tokensIn: usage.tokensIn, tokensOut: usage.tokensOut,
320
+ cacheReadTokens: usage.cacheReadTokens, cacheWriteTokens: usage.cacheWriteTokens,
382
321
  contextTokens,
383
322
  });
384
- if (calculated) {
385
- costBasis = "metered";
386
- costUsd = calculated.costUsd;
387
- priceRef = calculated.priceRef;
323
+ if (calculated && Number.isFinite(calculated.costUsd)) {
324
+ providerMetadata.listCostUsd = calculated.costUsd;
325
+ providerMetadata.listPriceRef = calculated.priceRef;
326
+ providerMetadata.listPriceSha256 = catalog.sha256;
388
327
  }
389
328
  }
390
329
 
391
- if (costBasis === "unknown" && (harness === "claude" || harness === "codex" || harness === "agy")) {
392
- costBasis = "unmetered";
393
- costUsd = null;
394
- priceRef = `subscription:${harness}`;
395
- }
396
-
397
- return {
330
+ const result: VnextProducerResult = {
398
331
  outcome,
399
332
  costBasis,
400
333
  costUsd,
@@ -407,15 +340,37 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
407
340
  harness,
408
341
  provider: resolved.provider,
409
342
  requestedModel: resolved.model,
410
- effectiveModel: resolved.model,
343
+ effectiveModel: parsed.effectiveModel ?? "unknown",
344
+ providerMetadata,
345
+ ...(effectUncertain ? { effectUncertain: true as const } : {}),
411
346
  ...(resolved.thinking !== undefined ? { thinking: resolved.thinking } : {}),
412
347
  agentRole: request.agentRole ?? request.agentId,
413
348
  ...(priceRef !== undefined ? { priceRef } : {}),
414
349
  };
350
+ try { providerMetadata.executionEvidenceSha256 = await evidence.finish(procResult, result); }
351
+ catch {
352
+ providerMetadata.evidenceWriteFailed = true;
353
+ return { ...result, outcome: "failed", effectUncertain: true };
354
+ }
355
+ return result;
356
+ }
357
+
358
+ const producer: VnextOneShotProducer = {
359
+ id: "oneshot",
360
+ produce(request): Promise<VnextProducerResult> {
361
+ const controller = new AbortController();
362
+ const signal = AbortSignal.any([request.signal, controller.signal]);
363
+ const pending = Promise.resolve().then(() => executeRequest({ ...request, signal }))
364
+ .finally(() => { running.delete(controller); });
365
+ running.set(controller, pending);
366
+ return pending;
415
367
  },
416
368
 
417
369
  async close(): Promise<void> {
418
- // One-shot producer holds no persistent process pools
370
+ closed = true;
371
+ const pending = [...running.values()];
372
+ for (const controller of running.keys()) controller.abort();
373
+ await Promise.allSettled(pending);
419
374
  },
420
375
  };
421
376
 
@@ -1,7 +1,7 @@
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
- import { DatabaseSync } from "node:sqlite";
4
+ import { DatabaseSync } from "./sqlite.ts";
5
5
  import { VnextConfigError, validateRunEvent, vnextCanonicalJson, type JsonValue, type VnextConfigIssue, type VnextConfigOptions } from "./vnext-config.ts";
6
6
  import { vnextUserStateRoot } from "./vnext-bindings.ts";
7
7
 
@@ -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);