@kontextmind/kxm 0.6.0 → 0.7.10

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 (175) 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 +5 -0
  14. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  15. package/docs/agent-skills.md +135 -0
  16. package/docs/architecture.md +1 -1
  17. package/docs/assignment-runner.md +21 -8
  18. package/docs/browser-automation.md +116 -0
  19. package/docs/configuration.md +11 -2
  20. package/docs/getting-started.md +21 -0
  21. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  22. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  23. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  24. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  25. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  26. package/docs/kb/how-to-take-over-session.md +32 -0
  27. package/docs/kb/why-authentication-disappeared.md +32 -0
  28. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  29. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  30. package/docs/kxm-handbook.md +3 -3
  31. package/docs/operations.md +24 -0
  32. package/docs/operator-pi-packages.md +63 -0
  33. package/docs/prompts/browser-annotate-feedback.md +41 -0
  34. package/docs/prompts/browser-diagnose-recover.md +38 -0
  35. package/docs/prompts/browser-explore.md +42 -0
  36. package/docs/prompts/browser-repro-fix.md +48 -0
  37. package/docs/prompts/browser-start.md +41 -0
  38. package/docs/prompts/browser-takeover.md +50 -0
  39. package/docs/skills/repo-work-delivery.md +107 -0
  40. package/docs/skills.md +2 -0
  41. package/docs/test-matrix.md +4 -3
  42. package/docs/troubleshooting.md +41 -1
  43. package/docs/vnext/validation.md +9 -0
  44. package/docs/webhook-workflows.md +2 -2
  45. package/examples/README.md +1 -1
  46. package/package.json +16 -17
  47. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  48. package/plugins/kxm/README.md +1 -1
  49. package/plugins/kxm/dist/cli.js +41620 -35578
  50. package/plugins/kxm/dist/core.js +271 -34
  51. package/plugins/kxm/dist/extension.js +7759 -86
  52. package/plugins/kxm/dist/mcp-server.js +75 -21
  53. package/plugins/kxm/dist/runtime.js +8218 -2328
  54. package/plugins/kxm/dist/server.js +3125 -2260
  55. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
  56. package/plugins/kxm/package.json +1 -1
  57. package/plugins/kxm/skills/SUITE.md +5 -0
  58. package/plugins/kxm/skills/hints.json +103 -0
  59. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  60. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  61. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  62. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  63. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  64. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  65. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  66. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  67. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  68. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  69. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  70. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  71. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  72. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  73. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  74. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  75. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  76. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  77. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  78. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  79. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  80. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  81. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  82. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  83. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  84. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  85. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  86. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  87. package/plugins/kxm/src/autocomplete.ts +9 -3
  88. package/plugins/kxm/src/browser.ts +603 -0
  89. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  90. package/plugins/kxm/src/cli/hub.ts +614 -0
  91. package/plugins/kxm/src/cli/roles.ts +615 -0
  92. package/plugins/kxm/src/cli/system.ts +906 -0
  93. package/plugins/kxm/src/cli/tasks.ts +364 -0
  94. package/plugins/kxm/src/cli/types.ts +270 -0
  95. package/plugins/kxm/src/cli/vnext.ts +698 -0
  96. package/plugins/kxm/src/cli/workflows.ts +699 -0
  97. package/plugins/kxm/src/cli.ts +362 -2849
  98. package/plugins/kxm/src/commands.ts +150 -8
  99. package/plugins/kxm/src/completion-install.ts +223 -0
  100. package/plugins/kxm/src/config.ts +7 -4
  101. package/plugins/kxm/src/context-packet.ts +172 -0
  102. package/plugins/kxm/src/database.ts +1 -1
  103. package/plugins/kxm/src/extension.ts +36 -1
  104. package/plugins/kxm/src/external-effects.ts +357 -8
  105. package/plugins/kxm/src/hub-env.ts +193 -0
  106. package/plugins/kxm/src/hub.ts +2 -4
  107. package/plugins/kxm/src/improve.ts +72 -0
  108. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  109. package/plugins/kxm/src/local-snapshot.ts +1 -1
  110. package/plugins/kxm/src/mcp-server.ts +1 -1
  111. package/plugins/kxm/src/model-inventory.ts +127 -0
  112. package/plugins/kxm/src/modes.ts +348 -0
  113. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  114. package/plugins/kxm/src/policy-draft.mjs +565 -0
  115. package/plugins/kxm/src/price-calc.ts +17 -18
  116. package/plugins/kxm/src/prices.ts +32 -16
  117. package/plugins/kxm/src/producers.ts +71 -0
  118. package/plugins/kxm/src/protocol.ts +111 -0
  119. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  120. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  121. package/plugins/kxm/src/role.ts +710 -0
  122. package/plugins/kxm/src/routing.ts +99 -1
  123. package/plugins/kxm/src/runtime.ts +4 -0
  124. package/plugins/kxm/src/safety-integrity.ts +76 -0
  125. package/plugins/kxm/src/session-work.ts +9 -2
  126. package/plugins/kxm/src/sqlite.ts +76 -0
  127. package/plugins/kxm/src/ssh-remote.ts +560 -0
  128. package/plugins/kxm/src/store.ts +1 -1
  129. package/plugins/kxm/src/studio-layout.ts +660 -17
  130. package/plugins/kxm/src/subagent-control.ts +312 -0
  131. package/plugins/kxm/src/suggest.ts +7 -13
  132. package/plugins/kxm/src/telemetry.ts +82 -0
  133. package/plugins/kxm/src/tui.ts +140 -0
  134. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  135. package/plugins/kxm/src/vnext-config.ts +53 -111
  136. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  137. package/plugins/kxm/src/vnext-engine.ts +214 -62
  138. package/plugins/kxm/src/vnext-harness.ts +336 -84
  139. package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
  140. package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
  141. package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
  142. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  143. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  144. package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
  145. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  146. package/plugins/kxm/src/workflow-manager.ts +392 -0
  147. package/plugins/kxm/src/workflow-tui.ts +255 -0
  148. package/plugins/kxm/src/workflow.ts +144 -0
  149. package/schemas/policy-draft/README.md +17 -0
  150. package/schemas/policy-draft/model.v2.schema.json +140 -0
  151. package/schemas/policy-draft/role.v2.schema.json +91 -0
  152. package/schemas/vnext/modes.schema.json +56 -0
  153. package/schemas/vnext/role.schema.json +76 -0
  154. package/schemas/vnext/run-event.schema.json +1 -0
  155. package/scripts/assignment-run.d.mts +1 -1
  156. package/scripts/assignment-run.mjs +44 -35
  157. package/scripts/check-generated.mjs +33 -9
  158. package/scripts/emit-codex-artifacts.mjs +255 -11
  159. package/scripts/harness-run.d.mts +12 -4
  160. package/scripts/harness-run.mjs +65 -17
  161. package/scripts/kxm-bump-version.mjs +146 -0
  162. package/scripts/kxm-hub.mjs +150 -2
  163. package/scripts/kxm-publish-npm.mjs +3 -1
  164. package/scripts/kxm-release-github.mjs +3 -1
  165. package/scripts/kxm.mjs +0 -0
  166. package/scripts/native-critic.d.mts +5 -0
  167. package/scripts/native-critic.mjs +60 -0
  168. package/.kxm/config/README.md +0 -5
  169. package/.kxm/config/agents.json +0 -43
  170. package/.kxm/config/env.example +0 -56
  171. package/.kxm/config/update.example.yaml +0 -9
  172. package/.kxm/config/workflows/fix.json +0 -160
  173. package/.kxm/config/workflows/jira-development.json +0 -116
  174. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  175. 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,150 @@ 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 unverifiedDescendants = procResult.unverifiedDescendants === true
256
+ || procResult.terminationRequested === true
257
+ || procResult.error?.message === "process_exit_unobserved"
258
+ || (procResult.observedChildExit === false && procResult.started !== false);
259
+ const effectUncertain = unverifiedDescendants || Boolean(procResult.signal);
260
+ const transportFailed = procResult.code !== 0 || Boolean(procResult.error) || effectUncertain;
261
+ const outcome = aborted ? "cancelled" : transportFailed || parsed.isError ? "failed"
262
+ : determineOutcome(parsed.text, request.allowedOutcomes);
263
+ const providerMetadata: Record<string, string | number | boolean> = {
264
+ processStatus: aborted ? "aborted" : transportFailed ? "failed" : "completed",
265
+ executionEvidenceId: evidence.id,
266
+ };
267
+ if (procResult.code !== null && Number.isFinite(procResult.code)) providerMetadata.processExitCode = procResult.code;
268
+ if (procResult.signal) providerMetadata.processSignal = procResult.signal;
269
+ if (procResult.observedChildExit !== undefined) providerMetadata.observedChildExit = procResult.observedChildExit;
270
+ if (procResult.started !== undefined) providerMetadata.processStarted = procResult.started;
271
+ if (procResult.terminationRequested !== undefined) providerMetadata.terminationRequested = procResult.terminationRequested;
272
+ if (unverifiedDescendants) providerMetadata.descendantEffects = "unverified";
273
+ if (procResult.error) {
274
+ const reason = procResult.error.message;
275
+ providerMetadata.processError = /^process_(aborted|timeout|output_limit|stdin_error|stdio_error|stdio_unclosed|exit_unobserved)$/.test(reason) ? reason : "process_error";
358
276
  }
359
-
360
277
  const latencyMs = Math.max(0, Date.now() - startTime);
361
278
  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) {
279
+ const validCount = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
280
+ const boundedCount = (field: string, value: unknown): number | null => {
281
+ if (validCount(value) && value <= 1_000_000) return value;
282
+ if (typeof value === "number" && Number.isFinite(value)) {
283
+ providerMetadata[`raw${field[0]!.toUpperCase()}${field.slice(1)}`] = value;
284
+ }
285
+ return null;
286
+ };
287
+ const tokensIn = boundedCount("tokensIn", usage?.tokensIn);
288
+ const tokensOut = boundedCount("tokensOut", usage?.tokensOut);
289
+ const cacheReadTokens = boundedCount("cacheReadTokens", usage?.cacheReadTokens);
290
+ const cacheWriteTokens = boundedCount("cacheWriteTokens", usage?.cacheWriteTokens);
291
+ const contextTokens = boundedCount("contextTokens", usage?.contextTokens);
292
+ if (typeof usage?.costUsd === "number" && Number.isFinite(usage.costUsd) && usage.costUsd >= 0) providerMetadata.providerReportedCostUsd = usage.costUsd;
293
+
294
+ // List estimates and provider-reported amounts are not proof of a bill.
295
+ let catalog: PriceCatalog | undefined;
296
+ try {
297
+ catalog = options.priceCatalog ? parsePriceCatalog(JSON.stringify(options.priceCatalog))
298
+ : loadPriceCatalog(options.projectRoot ?? process.cwd());
299
+ } catch { providerMetadata.priceCatalogUnavailable = true; }
300
+ // Without a freshness-aware vendor feed, don't silently quote an older
301
+ // local snapshot. Even today's hash-verified snapshot is only an estimate.
302
+ if (catalog && catalog.date !== new Date().toISOString().slice(0, 10)) {
303
+ providerMetadata.priceCatalogStale = true;
304
+ catalog = undefined;
305
+ }
306
+ const subscription = ["claude.ai", "ChatGPT", "antigravity-oauth"].includes(auth.authMethod ?? "");
307
+ const costBasis = subscription ? "unmetered" as const : "unknown" as const;
308
+ const costUsd = null;
309
+ const priceRef = subscription ? `subscription:${harness}` : undefined;
310
+ if (auth.authMethod) providerMetadata.authMethod = auth.authMethod;
311
+ const priceRow = catalog ? findModelPrice(catalog, resolved.model, resolved.provider) : undefined;
312
+ const flatTier = priceRow?.tiers.length === 1 && priceRow.tiers[0]?.upToContextTokens == null ? priceRow.tiers[0] : undefined;
313
+ // Unknown context occupancy cannot select a context tier. Claude reports
314
+ // uncached input separately; other protocols need their own price semantics.
315
+ if (catalog && harness === "claude" && flatTier
316
+ && validCount(usage?.tokensIn) && validCount(usage?.tokensOut)
317
+ && validCount(usage?.cacheReadTokens) && validCount(usage?.cacheWriteTokens)
318
+ && (usage.cacheReadTokens === 0 || flatTier.cacheReadPerMillion != null)
319
+ && (usage.cacheWriteTokens === 0 || flatTier.cacheWritePerMillion != null)) {
375
320
  const calculated = calculateModelCost(catalog, {
376
- model: resolved.model,
377
- provider: resolved.provider,
378
- tokensIn,
379
- tokensOut,
380
- cacheReadTokens,
381
- cacheWriteTokens,
321
+ model: resolved.model, provider: resolved.provider,
322
+ tokensIn: usage.tokensIn, tokensOut: usage.tokensOut,
323
+ cacheReadTokens: usage.cacheReadTokens, cacheWriteTokens: usage.cacheWriteTokens,
382
324
  contextTokens,
383
325
  });
384
- if (calculated) {
385
- costBasis = "metered";
386
- costUsd = calculated.costUsd;
387
- priceRef = calculated.priceRef;
326
+ if (calculated && Number.isFinite(calculated.costUsd)) {
327
+ providerMetadata.listCostUsd = calculated.costUsd;
328
+ providerMetadata.listPriceRef = calculated.priceRef;
329
+ providerMetadata.listPriceSha256 = catalog.sha256;
388
330
  }
389
331
  }
390
332
 
391
- if (costBasis === "unknown" && (harness === "claude" || harness === "codex" || harness === "agy")) {
392
- costBasis = "unmetered";
393
- costUsd = null;
394
- priceRef = `subscription:${harness}`;
395
- }
396
-
397
- return {
333
+ const result: VnextProducerResult = {
398
334
  outcome,
399
335
  costBasis,
400
336
  costUsd,
@@ -407,15 +343,37 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
407
343
  harness,
408
344
  provider: resolved.provider,
409
345
  requestedModel: resolved.model,
410
- effectiveModel: resolved.model,
346
+ effectiveModel: parsed.effectiveModel ?? "unknown",
347
+ providerMetadata,
348
+ ...(effectUncertain ? { effectUncertain: true as const } : {}),
411
349
  ...(resolved.thinking !== undefined ? { thinking: resolved.thinking } : {}),
412
350
  agentRole: request.agentRole ?? request.agentId,
413
351
  ...(priceRef !== undefined ? { priceRef } : {}),
414
352
  };
353
+ try { providerMetadata.executionEvidenceSha256 = await evidence.finish(procResult, result); }
354
+ catch {
355
+ providerMetadata.evidenceWriteFailed = true;
356
+ return { ...result, outcome: "failed", effectUncertain: true };
357
+ }
358
+ return result;
359
+ }
360
+
361
+ const producer: VnextOneShotProducer = {
362
+ id: "oneshot",
363
+ produce(request): Promise<VnextProducerResult> {
364
+ const controller = new AbortController();
365
+ const signal = AbortSignal.any([request.signal, controller.signal]);
366
+ const pending = Promise.resolve().then(() => executeRequest({ ...request, signal }))
367
+ .finally(() => { running.delete(controller); });
368
+ running.set(controller, pending);
369
+ return pending;
415
370
  },
416
371
 
417
372
  async close(): Promise<void> {
418
- // One-shot producer holds no persistent process pools
373
+ closed = true;
374
+ const pending = [...running.values()];
375
+ for (const controller of running.keys()) controller.abort();
376
+ await Promise.allSettled(pending);
419
377
  },
420
378
  };
421
379
 
@@ -8,8 +8,10 @@ import {
8
8
  type VnextProducerResult,
9
9
  } from "./vnext-engine.ts";
10
10
  import {
11
- probeHarnessAssignment,
11
+ probeHarnessAssignmentAsync,
12
+ type HarnessAssignmentProbeOptions,
12
13
  type HarnessInventory,
14
+ type HarnessStatus,
13
15
  } from "./vnext-harness.ts";
14
16
  import {
15
17
  calculateModelCost,
@@ -386,7 +388,7 @@ export interface VnextPiProducerOptions {
386
388
  priceCatalog?: PriceCatalog | undefined;
387
389
  env?: NodeJS.ProcessEnv | undefined;
388
390
  spawnProcess?: ((command: string, args: readonly string[], options: Record<string, unknown>) => PiRpcProcess) | undefined;
389
- probeHarness?: typeof probeHarnessAssignment | undefined;
391
+ probeHarness?: ((options: Omit<HarnessAssignmentProbeOptions, "runCommand"> & { signal?: AbortSignal | undefined }) => HarnessStatus | Promise<HarnessStatus>) | undefined;
390
392
  resolveModel?: ((agentId: string, runId: string) => { provider?: string | undefined; model?: string | undefined; thinking?: string | undefined } | undefined) | undefined;
391
393
  defaultModel?: string | undefined;
392
394
  defaultProvider?: string | undefined;
@@ -438,7 +440,7 @@ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): Vne
438
440
  return { provider: parsed.provider, model: parsed.model };
439
441
  }
440
442
 
441
- function checkPiAuth(provider: string, model: string): void {
443
+ async function checkPiAuth(provider: string, model: string, signal: AbortSignal): Promise<void> {
442
444
  if (options.inventory) {
443
445
  const piEntry = options.inventory.harnesses.find((h) => h.id === "pi");
444
446
  if (!piEntry || !piEntry.detected || piEntry.authenticated === false) {
@@ -446,12 +448,14 @@ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): Vne
446
448
  }
447
449
  }
448
450
 
449
- const probeFn = options.probeHarness ?? probeHarnessAssignment;
450
- const probe = probeFn({
451
+ const probeFn = options.probeHarness ?? probeHarnessAssignmentAsync;
452
+ const probe = await probeFn({
451
453
  harness: "pi",
452
454
  provider,
453
455
  model,
454
456
  env: options.env,
457
+ signal,
458
+ timeoutMs: 10_000,
455
459
  });
456
460
 
457
461
  if (!probe.detected) {
@@ -538,8 +542,8 @@ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): Vne
538
542
  const startTime = Date.now();
539
543
  const resolved = resolveModelForRequest(request);
540
544
 
541
- // Preflight auth check - fails closed
542
- checkPiAuth(resolved.provider, resolved.model);
545
+ // Preflight auth check - fails closed via the async bounded probe
546
+ await checkPiAuth(resolved.provider, resolved.model, request.signal);
543
547
 
544
548
  // Get or create session
545
549
  const session = await getOrCreateSession(request, resolved.model);
@@ -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);