@kontextmind/kxm 0.7.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 (94) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/roles/writer.yaml +2 -0
  3. package/docs/README.md +3 -0
  4. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  5. package/docs/agent-skills.md +19 -2
  6. package/docs/browser-automation.md +116 -0
  7. package/docs/configuration.md +10 -1
  8. package/docs/getting-started.md +21 -0
  9. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  10. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  11. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  12. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  13. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  14. package/docs/kb/how-to-take-over-session.md +32 -0
  15. package/docs/kb/why-authentication-disappeared.md +32 -0
  16. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  17. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  18. package/docs/operations.md +24 -0
  19. package/docs/prompts/browser-annotate-feedback.md +41 -0
  20. package/docs/prompts/browser-diagnose-recover.md +38 -0
  21. package/docs/prompts/browser-explore.md +42 -0
  22. package/docs/prompts/browser-repro-fix.md +48 -0
  23. package/docs/prompts/browser-start.md +41 -0
  24. package/docs/prompts/browser-takeover.md +50 -0
  25. package/docs/skills/repo-work-delivery.md +5 -0
  26. package/docs/skills.md +2 -0
  27. package/docs/troubleshooting.md +22 -1
  28. package/package.json +1 -1
  29. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  30. package/plugins/kxm/dist/cli.js +41203 -38364
  31. package/plugins/kxm/dist/core.js +57 -0
  32. package/plugins/kxm/dist/extension.js +40 -3
  33. package/plugins/kxm/dist/mcp-server.js +1 -1
  34. package/plugins/kxm/dist/runtime.js +1582 -81
  35. package/plugins/kxm/dist/server.js +129 -4
  36. package/plugins/kxm/dist/vnext-runtime-supervisor.js +221 -41
  37. package/plugins/kxm/package.json +1 -1
  38. package/plugins/kxm/skills/hints.json +30 -0
  39. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  40. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  41. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  42. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  43. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  44. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  45. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  46. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -0
  47. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +9 -2
  48. package/plugins/kxm/src/autocomplete.ts +1 -1
  49. package/plugins/kxm/src/browser.ts +603 -0
  50. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  51. package/plugins/kxm/src/cli/hub.ts +614 -0
  52. package/plugins/kxm/src/cli/roles.ts +615 -0
  53. package/plugins/kxm/src/cli/system.ts +906 -0
  54. package/plugins/kxm/src/cli/tasks.ts +364 -0
  55. package/plugins/kxm/src/cli/types.ts +270 -0
  56. package/plugins/kxm/src/cli/vnext.ts +698 -0
  57. package/plugins/kxm/src/cli/workflows.ts +699 -0
  58. package/plugins/kxm/src/cli.ts +238 -3791
  59. package/plugins/kxm/src/completion-install.ts +223 -0
  60. package/plugins/kxm/src/database.ts +1 -1
  61. package/plugins/kxm/src/external-effects.ts +1 -1
  62. package/plugins/kxm/src/hub-env.ts +193 -0
  63. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  64. package/plugins/kxm/src/local-snapshot.ts +1 -1
  65. package/plugins/kxm/src/mcp-server.ts +1 -1
  66. package/plugins/kxm/src/model-inventory.ts +8 -8
  67. package/plugins/kxm/src/modes.ts +348 -0
  68. package/plugins/kxm/src/protocol.ts +111 -0
  69. package/plugins/kxm/src/role.ts +335 -0
  70. package/plugins/kxm/src/runtime.ts +4 -0
  71. package/plugins/kxm/src/safety-integrity.ts +76 -0
  72. package/plugins/kxm/src/sqlite.ts +76 -0
  73. package/plugins/kxm/src/ssh-remote.ts +560 -0
  74. package/plugins/kxm/src/store.ts +1 -1
  75. package/plugins/kxm/src/subagent-control.ts +312 -0
  76. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  77. package/plugins/kxm/src/vnext-config.ts +38 -1
  78. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  79. package/plugins/kxm/src/vnext-engine.ts +16 -0
  80. package/plugins/kxm/src/vnext-harness.ts +92 -22
  81. package/plugins/kxm/src/vnext-oneshot-evidence.ts +39 -7
  82. package/plugins/kxm/src/vnext-oneshot-process.ts +46 -8
  83. package/plugins/kxm/src/vnext-oneshot-producer.ts +22 -1
  84. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  85. package/plugins/kxm/src/vnext-runtime-store.ts +1 -1
  86. package/plugins/kxm/src/vnext-runtime-supervisor.ts +5 -3
  87. package/plugins/kxm/src/workflow-tui.ts +1 -1
  88. package/plugins/kxm/src/workflow.ts +144 -0
  89. package/schemas/vnext/modes.schema.json +56 -0
  90. package/scripts/kxm-bump-version.mjs +146 -0
  91. package/scripts/kxm-hub.mjs +145 -3
  92. package/scripts/kxm-publish-npm.mjs +3 -1
  93. package/scripts/kxm-release-github.mjs +3 -1
  94. package/scripts/kxm.mjs +0 -0
@@ -3,10 +3,37 @@ import { lstat, mkdir, open, rename } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import type { VnextOneShotProcessResult } from "./vnext-oneshot-process.ts";
5
5
 
6
+ export const ONESHOT_EVIDENCE_SCHEMA = "kxm.oneshot-evidence.v2" as const;
7
+ const RETIRED_ONESHOT_EVIDENCE_SCHEMAS = new Set(["kxm.oneshot-evidence.v1"]);
8
+
6
9
  const TEXT_LIMIT = 4 * 1024 * 1024;
10
+ const ARGV_LIMIT = 64 * 1024;
7
11
  const RECORD_LIMIT = 16 * 1024 * 1024;
8
12
  const digest = (value: string): string => `sha256:${createHash("sha256").update(value).digest("hex")}`;
9
13
 
14
+ export interface OneShotEvidenceRecord {
15
+ schema: typeof ONESHOT_EVIDENCE_SCHEMA;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ /** Brake: retired and unknown persisted schema ids are never aliased or upgraded. */
20
+ export function parseOneShotEvidenceRecord(bytes: string): OneShotEvidenceRecord {
21
+ let parsed: unknown;
22
+ try {
23
+ parsed = JSON.parse(bytes) as unknown;
24
+ } catch {
25
+ throw new Error("oneshot_evidence_record_invalid");
26
+ }
27
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
28
+ throw new Error("oneshot_evidence_record_invalid");
29
+ }
30
+ const schema = (parsed as { schema?: unknown }).schema;
31
+ if (typeof schema !== "string") throw new Error("oneshot_evidence_record_invalid");
32
+ if (RETIRED_ONESHOT_EVIDENCE_SCHEMAS.has(schema)) throw new Error("oneshot_evidence_schema_retired");
33
+ if (schema !== ONESHOT_EVIDENCE_SCHEMA) throw new Error("oneshot_evidence_schema_unknown");
34
+ return parsed as OneShotEvidenceRecord;
35
+ }
36
+
10
37
  /** Private diagnostic evidence, never a peer reply or acceptance authority.
11
38
  * Retained until operator archival; no automatic deletion of failed attempts.
12
39
  * Each record is bounded; inability to reserve/write evidence fails closed.
@@ -32,11 +59,13 @@ export async function beginOneShotEvidence(root: string, intent: {
32
59
  const redact = (value: string): string => {
33
60
  let text = value;
34
61
  for (const secret of secrets) text = text.replaceAll(secret, "[REDACTED]");
35
- return text.replace(/(\b(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\b["']?\s*[:=]\s*["']?)(?:Bearer\s+)?[^\s"',;}]+/gi, "$1[REDACTED]");
62
+ return text
63
+ .replace(/(\b(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\b["']?\s*[:=]\s*["']?)(?:Bearer\s+)?[^\s"',;}]+/gi, "$1[REDACTED]")
64
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
36
65
  };
37
- const bounded = (value: string): { text: string; truncated: boolean; sha256: string } => {
66
+ const bounded = (value: string, max = TEXT_LIMIT): { text: string; truncated: boolean; sha256: string } => {
38
67
  const bytes = Buffer.from(redact(value));
39
- return { text: bytes.subarray(0, TEXT_LIMIT).toString("utf8"), truncated: bytes.length > TEXT_LIMIT, sha256: digest(value) };
68
+ return { text: bytes.subarray(0, max).toString("utf8"), truncated: bytes.length > max, sha256: digest(redact(value)) };
40
69
  };
41
70
  const write = async (name: string, value: unknown): Promise<string> => {
42
71
  const current = await lstat(dir);
@@ -46,6 +75,7 @@ export async function beginOneShotEvidence(root: string, intent: {
46
75
  }
47
76
  const bytes = JSON.stringify(value);
48
77
  if (Buffer.byteLength(bytes) > RECORD_LIMIT) throw new Error("oneshot_evidence_record_limit");
78
+ parseOneShotEvidenceRecord(bytes);
49
79
  const target = join(dir, name);
50
80
  const partial = `${target}.partial`;
51
81
  const file = await open(partial, "wx", 0o600);
@@ -59,11 +89,12 @@ export async function beginOneShotEvidence(root: string, intent: {
59
89
  return digest(bytes);
60
90
  };
61
91
  await write("intent.json", {
62
- schema: "kxm.oneshot-evidence.v1", id, recordedAt: new Date().toISOString(),
92
+ schema: ONESHOT_EVIDENCE_SCHEMA, id, recordedAt: new Date().toISOString(),
63
93
  runId: intent.runId, stepId: intent.stepId, attemptId: intent.attemptId, assignmentId: intent.assignmentId,
64
94
  harness: intent.harness, provider: intent.provider, requestedModel: intent.model,
65
- cwd: intent.cwd, command: intent.command,
66
- argv: bounded(JSON.stringify(intent.args)), stdin: intent.input === undefined ? null : bounded(intent.input),
95
+ cwd: intent.cwd, command: redact(intent.command),
96
+ argv: bounded(JSON.stringify(intent.args), ARGV_LIMIT), stdin: intent.input === undefined ? null : bounded(intent.input),
97
+ env: null,
67
98
  acceptance: false,
68
99
  });
69
100
  let finished = false;
@@ -73,10 +104,11 @@ export async function beginOneShotEvidence(root: string, intent: {
73
104
  if (finished) throw new Error("oneshot_evidence_already_finished");
74
105
  finished = true;
75
106
  return write("result.json", {
76
- schema: "kxm.oneshot-evidence.v1", id, recordedAt: new Date().toISOString(),
107
+ schema: ONESHOT_EVIDENCE_SCHEMA, id, recordedAt: new Date().toISOString(),
77
108
  stdout: bounded(result.stdout), stderr: bounded(result.stderr),
78
109
  code: result.code, signal: result.signal ?? null, started: result.started ?? null,
79
110
  observedChildExit: result.observedChildExit ?? null, terminationRequested: result.terminationRequested ?? null,
111
+ unverifiedDescendants: result.unverifiedDescendants ?? null,
80
112
  error: result.error ? bounded(result.error.message) : null,
81
113
  observation: bounded(JSON.stringify(observation)), acceptance: false,
82
114
  });
@@ -9,6 +9,11 @@ export interface VnextOneShotProcessResult {
9
9
  started?: boolean | undefined;
10
10
  /** Stop was requested; direct-child reaping cannot attest escaped descendants. */
11
11
  terminationRequested?: boolean | undefined;
12
+ /**
13
+ * Direct-child close/reap is not descendant death. Unobserved process-group
14
+ * members stay unverified: never success, never a forged descendant failure.
15
+ */
16
+ unverifiedDescendants?: boolean | undefined;
12
17
  error?: Error | undefined;
13
18
  }
14
19
 
@@ -18,6 +23,8 @@ export interface VnextOneShotSpawnOptions {
18
23
  input?: string | undefined;
19
24
  timeoutMs?: number | undefined;
20
25
  signal?: AbortSignal | undefined;
26
+ /** Only allowlisted Windows `.cmd` probes may set this; never a user string. */
27
+ shell?: boolean | undefined;
21
28
  }
22
29
  export type VnextOneShotSpawn = (command: string, args: readonly string[], options: VnextOneShotSpawnOptions) => Promise<VnextOneShotProcessResult>;
23
30
 
@@ -26,6 +33,26 @@ const KILL_GRACE_MS = 250;
26
33
  const DRAIN_GRACE_MS = 500;
27
34
  const REAP_GRACE_MS = 1000;
28
35
 
36
+ /**
37
+ * POSIX process-group tree kill with fallback to direct child kill.
38
+ * Ensures orphaned subshells, test workers, and background daemons are pruned on cancellation/timeout.
39
+ */
40
+ export function killProcessTree(child: ReturnType<typeof spawn>, signal: NodeJS.Signals = "SIGKILL"): void {
41
+ if (process.platform !== "win32" && child.pid) {
42
+ try {
43
+ process.kill(-child.pid, signal);
44
+ return;
45
+ } catch {
46
+ // Fall through to direct child kill if process group is unavailable
47
+ }
48
+ }
49
+ try {
50
+ child.kill(signal);
51
+ } catch {
52
+ // Direct kill error if child already exited
53
+ }
54
+ }
55
+
29
56
  /** Bound both execution and pipe drain. Observing exit is not proof of descendant death. */
30
57
  export function defaultSpawn(command: string, args: readonly string[], options: VnextOneShotSpawnOptions): Promise<VnextOneShotProcessResult> {
31
58
  if (options.signal?.aborted) {
@@ -51,6 +78,9 @@ export function defaultSpawn(command: string, args: readonly string[], options:
51
78
  let child: ReturnType<typeof spawn>;
52
79
  const finish = (): void => {
53
80
  if (finished) return;
81
+ // Direct-child close must not cancel SIGTERM → SIGKILL. Refuse to settle
82
+ // until the group kill has been sent, then reap on the existing timers.
83
+ if (stopping && !killSent) return;
54
84
  finished = true;
55
85
  for (const timer of [wallTimer, killTimer, drainTimer, reapTimer]) clearTimeout(timer);
56
86
  options.signal?.removeEventListener("abort", onAbort);
@@ -60,13 +90,21 @@ export function defaultSpawn(command: string, args: readonly string[], options:
60
90
  child?.unref();
61
91
  const started = Boolean(child?.pid);
62
92
  if (started && !observedChildExit) error ??= new Error("process_exit_unobserved");
63
- resolve({ stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8"), code, signal, started, observedChildExit, terminationRequested: stopping, ...(error ? { error } : {}) });
93
+ const unverifiedDescendants = stopping || (started && !observedChildExit);
94
+ resolve({
95
+ stdout: Buffer.concat(stdout).toString("utf8"),
96
+ stderr: Buffer.concat(stderr).toString("utf8"),
97
+ code,
98
+ signal,
99
+ started,
100
+ observedChildExit,
101
+ terminationRequested: stopping,
102
+ ...(unverifiedDescendants ? { unverifiedDescendants: true } : {}),
103
+ ...(error ? { error } : {}),
104
+ });
64
105
  };
65
106
  const kill = (requested: NodeJS.Signals): void => {
66
- try {
67
- if (process.platform !== "win32" && child.pid) process.kill(-child.pid, requested);
68
- else child.kill(requested);
69
- } catch { /* Reaping, not kill() success, establishes direct-child exit. */ }
107
+ killProcessTree(child, requested);
70
108
  };
71
109
  const stop = (reason: string): void => {
72
110
  if (finished) return;
@@ -91,7 +129,7 @@ export function defaultSpawn(command: string, args: readonly string[], options:
91
129
  cwd: options.cwd,
92
130
  env: options.env ?? process.env,
93
131
  stdio: ["pipe", "pipe", "pipe"],
94
- shell: false,
132
+ shell: options.shell === true,
95
133
  windowsHide: true,
96
134
  detached: process.platform !== "win32",
97
135
  });
@@ -136,8 +174,8 @@ export function defaultSpawn(command: string, args: readonly string[], options:
136
174
  code = exitCode;
137
175
  signal = exitSignal;
138
176
  // The leader can exit and close its pipes while a descendant ignores TERM.
139
- // Never cancel the pending group escalation just because close arrived.
140
- if (!stopping || killSent) finish();
177
+ // finish() no-ops until SIGKILL when escalation is in flight.
178
+ finish();
141
179
  });
142
180
  options.signal?.addEventListener("abort", onAbort, { once: true });
143
181
  if (options.signal?.aborted) onAbort();
@@ -195,6 +195,24 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
195
195
  args = ["--model", resolved.model,
196
196
  ...(resolved.thinking ? ["--reasoning-effort", resolved.thinking] : []),
197
197
  ...permissionArgs, "--output-format", "json"];
198
+ } else if (harness === "agy") {
199
+ args = [
200
+ "-m",
201
+ resolved.model,
202
+ ...permissionArgs,
203
+ "--output-format",
204
+ "json",
205
+ "-p",
206
+ ];
207
+ } else if (harness === "kimi") {
208
+ args = [
209
+ "-m",
210
+ resolved.model,
211
+ ...permissionArgs,
212
+ "--output-format",
213
+ "stream-json",
214
+ "-p",
215
+ ];
198
216
  } else {
199
217
  throw new Error(`oneshot_harness_unsupported: ${harness} permission_profile_unaudited`);
200
218
  }
@@ -234,9 +252,11 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
234
252
  const aborted = request.signal.aborted || procResult.error?.message === "process_aborted";
235
253
  // Process groups are cleanup, not containment. A detached descendant or
236
254
  // provider leader may outlive the client; interrupted effects need recovery.
237
- const effectUncertain = procResult.terminationRequested === true || Boolean(procResult.signal)
255
+ const unverifiedDescendants = procResult.unverifiedDescendants === true
256
+ || procResult.terminationRequested === true
238
257
  || procResult.error?.message === "process_exit_unobserved"
239
258
  || (procResult.observedChildExit === false && procResult.started !== false);
259
+ const effectUncertain = unverifiedDescendants || Boolean(procResult.signal);
240
260
  const transportFailed = procResult.code !== 0 || Boolean(procResult.error) || effectUncertain;
241
261
  const outcome = aborted ? "cancelled" : transportFailed || parsed.isError ? "failed"
242
262
  : determineOutcome(parsed.text, request.allowedOutcomes);
@@ -249,6 +269,7 @@ export function createVnextOneShotProducer(options: VnextOneShotProducerOptions
249
269
  if (procResult.observedChildExit !== undefined) providerMetadata.observedChildExit = procResult.observedChildExit;
250
270
  if (procResult.started !== undefined) providerMetadata.processStarted = procResult.started;
251
271
  if (procResult.terminationRequested !== undefined) providerMetadata.terminationRequested = procResult.terminationRequested;
272
+ if (unverifiedDescendants) providerMetadata.descendantEffects = "unverified";
252
273
  if (procResult.error) {
253
274
  const reason = procResult.error.message;
254
275
  providerMetadata.processError = /^process_(aborted|timeout|output_limit|stdin_error|stdio_error|stdio_unclosed|exit_unobserved)$/.test(reason) ? reason : "process_error";
@@ -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
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
 
@@ -476,7 +476,7 @@ async function startVnextRuntimeSupervisorInner(
476
476
  });
477
477
  })();
478
478
 
479
- let drivePromise: Promise<unknown>;
479
+ let drivePromise: Promise<unknown> | undefined;
480
480
  let earlyError: unknown;
481
481
  try {
482
482
  const scheduler = VnextRunScheduler.for(context, bundle);
@@ -484,7 +484,6 @@ async function startVnextRuntimeSupervisorInner(
484
484
  drivePromise.catch((err) => { earlyError = err; });
485
485
  } catch (err) {
486
486
  earlyError = err;
487
- drivePromise = Promise.reject(err);
488
487
  }
489
488
 
490
489
  // Yield microtask to catch synchronous duplicate queue check (e.g. run_busy)
@@ -503,11 +502,14 @@ async function startVnextRuntimeSupervisorInner(
503
502
  throw earlyError;
504
503
  }
505
504
 
506
- void drivePromise.finally(async () => {
505
+ void drivePromise!.finally(async () => {
507
506
  cleanupActive();
508
507
  if ("close" in producer && typeof producer.close === "function") {
509
508
  try { await producer.close(); } catch { /* ignore */ }
510
509
  }
510
+ }).catch(() => {
511
+ // Derived finally() re-rejects when the admitted drive or cleanup
512
+ // throws; void does not consume that. Keep the request 202.
511
513
  });
512
514
 
513
515
  sendJson(response, 202, {
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
- import { DatabaseSync } from "node:sqlite";
8
+ import { DatabaseSync } from "./sqlite.ts";
9
9
  import { repoConfigDirectory } from "./config.ts";
10
10
  import { readRoutingRecords, telemetryPath } from "./telemetry.ts";
11
11
 
@@ -3,11 +3,17 @@ import {
3
3
  MAX_MESSAGE_TTL_MS,
4
4
  MIN_MESSAGE_TTL_MS,
5
5
  ProtocolError,
6
+ TERMINAL_RECEIPT_SCHEMA,
6
7
  newId,
7
8
  requireString,
9
+ validateTerminalReceipt,
8
10
  type ImprovementArea,
9
11
  type JournalCategory,
10
12
  type MessageRecord,
13
+ type TerminalReceipt,
14
+ type TerminalReceiptEvidence,
15
+ type TerminalReceiptMetrics,
16
+ type TerminalReceiptStatus,
11
17
  type WorkflowCheckpointStatus,
12
18
  type WorkflowEvidenceInput,
13
19
  type WorkflowEvidenceReference,
@@ -18,11 +24,16 @@ import {
18
24
  export type {
19
25
  ImprovementArea,
20
26
  JournalCategory,
27
+ TerminalReceipt,
28
+ TerminalReceiptEvidence,
29
+ TerminalReceiptMetrics,
30
+ TerminalReceiptStatus,
21
31
  WorkflowCheckpointStatus,
22
32
  WorkflowEvidenceInput,
23
33
  WorkflowEvidenceReference,
24
34
  WorkflowEvidenceReferenceInput,
25
35
  } from "./protocol.ts";
36
+ export { TERMINAL_RECEIPT_SCHEMA, validateTerminalReceipt } from "./protocol.ts";
26
37
 
27
38
  export type WorkflowRunStatus = "running" | "waiting" | "completed" | "failed";
28
39
  export type WorkflowStageStatus = "pending" | "in_progress" | "waiting" | WorkflowCheckpointStatus;
@@ -153,6 +164,8 @@ export interface WorkflowStageDefinition {
153
164
  instructions: string;
154
165
  requiredEvidence: string[];
155
166
  maxAttempts: number;
167
+ /** Bounded in-place retry limit before audit escalation (default: 2). */
168
+ autoResumeLimit?: number;
156
169
  area?: ImprovementArea;
157
170
  evidencePolicies?: WorkflowEvidencePolicies;
158
171
  /** Typed outcome map (v0.5). Keys are outcome identities ("passed",
@@ -250,6 +263,13 @@ export interface WorkflowStageState extends WorkflowStageDefinition {
250
263
  startedAt?: string;
251
264
  completedAt?: string;
252
265
  updatedAt?: string;
266
+ receipt?: TerminalReceipt;
267
+ auditEscalation?: {
268
+ reason: string;
269
+ timestamp: string;
270
+ ruling?: string;
271
+ receipt?: TerminalReceipt;
272
+ };
253
273
  }
254
274
 
255
275
  export interface CapturedOracle {
@@ -1003,12 +1023,18 @@ export function parseWorkflowDefinitions(
1003
1023
  && (!Number.isInteger(stageMaxTransitions) || (stageMaxTransitions as number) < 1 || (stageMaxTransitions as number) > 100)) {
1004
1024
  throw new Error(`stage ${stageId} maxTransitions must be an integer between 1 and 100`);
1005
1025
  }
1026
+ const autoResumeLimit = stage.autoResumeLimit;
1027
+ if (autoResumeLimit !== undefined
1028
+ && (!Number.isInteger(autoResumeLimit) || (autoResumeLimit as number) < 1 || (autoResumeLimit as number) > 20)) {
1029
+ throw new Error(`stage ${stageId} autoResumeLimit must be an integer between 1 and 20`);
1030
+ }
1006
1031
  return {
1007
1032
  id: stageId,
1008
1033
  label: requireString(stage.label ?? stageId, "stage.label", { max: 128 }),
1009
1034
  instructions: requireString(stage.instructions, "stage.instructions", { max: 4_000 }),
1010
1035
  requiredEvidence,
1011
1036
  maxAttempts: maxAttempts as number,
1037
+ ...(autoResumeLimit !== undefined ? { autoResumeLimit: autoResumeLimit as number } : {}),
1012
1038
  ...(area ? { area } : {}),
1013
1039
  ...(evidencePolicies ? { evidencePolicies } : {}),
1014
1040
  ...(on ? { on } : {}),
@@ -1377,6 +1403,31 @@ export function checkpointRun(
1377
1403
  delete run.currentStage;
1378
1404
  return { retry: false, completed: false, run };
1379
1405
  }
1406
+ if (stage.autoResumeLimit !== undefined && stage.attempts >= stage.autoResumeLimit) {
1407
+ stage.status = "in_progress";
1408
+ const reason = summary || `autoResumeLimit of ${stage.autoResumeLimit} reached on stage ${stage.id}`;
1409
+ const receipt = validateTerminalReceipt({
1410
+ schema: TERMINAL_RECEIPT_SCHEMA,
1411
+ status: "audit_escalation",
1412
+ seat: stage.id,
1413
+ runId: run.id,
1414
+ stageId: stage.id,
1415
+ timestamp,
1416
+ host: "pi",
1417
+ model: "default",
1418
+ escalationReason: reason,
1419
+ });
1420
+ const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
1421
+ waitForWorkflowSignal(run, stage.id, "audit_escalation", reason, timestamp, expiresAt);
1422
+ stage.receipt = receipt;
1423
+ stage.auditEscalation = {
1424
+ reason,
1425
+ timestamp,
1426
+ receipt,
1427
+ };
1428
+ return { retry: false, completed: false, run };
1429
+ }
1430
+ stage.status = status;
1380
1431
  // Declared failure outcomes create typed transitions (bounded); the
1381
1432
  // outcome identity defaults to the checkpoint status.
1382
1433
  const outcomeKey = outcome ?? status;
@@ -1546,3 +1597,96 @@ export function approveWorkflowDegradation(
1546
1597
  run.updatedAt = timestamp;
1547
1598
  return { run, approval, created: true };
1548
1599
  }
1600
+
1601
+ export function escalateWorkflowStage(
1602
+ run: WorkflowRun,
1603
+ stageId: string,
1604
+ reason: string,
1605
+ timestamp: string,
1606
+ options: {
1607
+ seat?: string;
1608
+ host?: string;
1609
+ model?: string;
1610
+ evidence?: TerminalReceiptEvidence;
1611
+ metrics?: TerminalReceiptMetrics;
1612
+ } = {},
1613
+ ): { run: WorkflowRun; receipt: TerminalReceipt } {
1614
+ if (run.status !== "running") throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_terminal");
1615
+ const stage = run.stages.find((s) => s.id === stageId);
1616
+ if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
1617
+ if (stage.id !== run.currentStage || stage.status !== "in_progress") {
1618
+ throw new ProtocolError(409, `stage ${stageId} is not currently active`, "workflow_stage_out_of_order");
1619
+ }
1620
+
1621
+ const receipt: TerminalReceipt = validateTerminalReceipt({
1622
+ schema: TERMINAL_RECEIPT_SCHEMA,
1623
+ status: "audit_escalation",
1624
+ seat: options.seat ?? stage.id,
1625
+ runId: run.id,
1626
+ stageId,
1627
+ timestamp,
1628
+ host: options.host ?? "pi",
1629
+ model: options.model ?? "default",
1630
+ escalationReason: reason,
1631
+ ...(options.evidence ? { evidence: options.evidence } : {}),
1632
+ ...(options.metrics ? { metrics: options.metrics } : {}),
1633
+ });
1634
+
1635
+ const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
1636
+ waitForWorkflowSignal(run, stageId, "audit_escalation", reason, timestamp, expiresAt);
1637
+
1638
+ stage.receipt = receipt;
1639
+ stage.auditEscalation = {
1640
+ reason,
1641
+ timestamp,
1642
+ receipt,
1643
+ };
1644
+
1645
+ return { run, receipt };
1646
+ }
1647
+
1648
+ export function resumeWorkflowFromRuling(
1649
+ run: WorkflowRun,
1650
+ ruling: string,
1651
+ timestamp: string,
1652
+ options: {
1653
+ status?: WorkflowCheckpointStatus;
1654
+ evidence?: WorkflowEvidenceInput;
1655
+ } = {},
1656
+ ): { retry: boolean; completed: boolean; run: WorkflowRun; stageId: string } {
1657
+ if (run.status !== "waiting" || !run.waiting) {
1658
+ throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_not_waiting");
1659
+ }
1660
+ if (run.waiting.signalKey !== "audit_escalation") {
1661
+ throw new ProtocolError(
1662
+ 409,
1663
+ `workflow is waiting for signal '${run.waiting.signalKey}', not 'audit_escalation'`,
1664
+ "workflow_signal_mismatch",
1665
+ );
1666
+ }
1667
+
1668
+ const stageId = run.waiting.stageId;
1669
+ const stage = run.stages.find((s) => s.id === stageId);
1670
+ if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
1671
+
1672
+ const status = options.status ?? "passed";
1673
+ const evidence = options.evidence ?? {};
1674
+
1675
+ if (stage.auditEscalation) {
1676
+ stage.auditEscalation.ruling = ruling;
1677
+ if (stage.auditEscalation.receipt) {
1678
+ stage.auditEscalation.receipt.ruling = ruling;
1679
+ }
1680
+ }
1681
+ stage.summary = `Resumed by operator ruling: ${ruling}`;
1682
+
1683
+ return resumeWorkflowFromSignal(
1684
+ run,
1685
+ "audit_escalation",
1686
+ status,
1687
+ `Resumed: ${ruling}`,
1688
+ evidence,
1689
+ timestamp,
1690
+ );
1691
+ }
1692
+
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.kxm.dev/vnext/modes.schema.json",
4
+ "title": "KXM Modes Configuration",
5
+ "description": "Declarative major modes and domain modules for selective tool loading and context scoping",
6
+ "type": "object",
7
+ "required": ["schema", "majorModes"],
8
+ "properties": {
9
+ "schema": { "const": "kxm.modes.v1" },
10
+ "majorModes": {
11
+ "type": "object",
12
+ "additionalProperties": {
13
+ "type": "object",
14
+ "required": ["baseTools"],
15
+ "properties": {
16
+ "description": { "type": "string" },
17
+ "baseTools": {
18
+ "type": "array",
19
+ "items": { "type": "string" }
20
+ },
21
+ "contextFiles": {
22
+ "type": "array",
23
+ "items": { "type": "string" }
24
+ },
25
+ "thinkingLevel": {
26
+ "type": "string",
27
+ "enum": ["low", "medium", "high", "xhigh"]
28
+ },
29
+ "model": { "type": "string" }
30
+ },
31
+ "additionalProperties": false
32
+ }
33
+ },
34
+ "domains": {
35
+ "type": "object",
36
+ "additionalProperties": {
37
+ "type": "object",
38
+ "required": ["tools"],
39
+ "properties": {
40
+ "description": { "type": "string" },
41
+ "tools": {
42
+ "type": "array",
43
+ "items": { "type": "string" }
44
+ },
45
+ "contextFiles": {
46
+ "type": "array",
47
+ "items": { "type": "string" }
48
+ },
49
+ "promptSnippet": { "type": "string" }
50
+ },
51
+ "additionalProperties": false
52
+ }
53
+ }
54
+ },
55
+ "additionalProperties": false
56
+ }