@kontextmind/kxm 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,86 @@
1
+ import { join } from "node:path";
2
+ import { verifyArtifactExists } from "./artifacts-exist.ts";
3
+ import type { VnextRuntimeContext } from "./vnext-runtime.ts";
4
+
5
+ export interface VnextArtifactsObservation {
6
+ completeness: "complete";
7
+ spawned: 0;
8
+ pid: null;
9
+ exitCode: null;
10
+ signal: null;
11
+ exitObserved: 0;
12
+ closeObserved: 0;
13
+ stopCause: "none";
14
+ signalsAttempted: "none";
15
+ errorClass: null;
16
+ stdoutSha256: null;
17
+ stdoutBytes: null;
18
+ stdoutComplete: null;
19
+ stderrSha256: null;
20
+ stderrBytes: null;
21
+ stderrComplete: null;
22
+ checkedCount: number;
23
+ failedCount: number;
24
+ elapsedMs: number;
25
+ startedAt: string;
26
+ finishedAt: string;
27
+ }
28
+
29
+ export interface VnextArtifactsGateSeams {
30
+ afterPathChecked?: ((input: {
31
+ path: string;
32
+ index: number;
33
+ checkedCount: number;
34
+ failedCount: number;
35
+ }) => void) | undefined;
36
+ }
37
+
38
+ export const vnextArtifactsGateSeams: VnextArtifactsGateSeams = {};
39
+
40
+ /**
41
+ * Check every pinned path under control `.kxm/assets`. Normal helper failures
42
+ * are complete failed facts. Exceptions after checks begin must propagate.
43
+ */
44
+ export function evaluateArtifactsGate(
45
+ context: VnextRuntimeContext,
46
+ definition: { readonly kind: "artifacts-exist"; readonly paths: readonly string[] },
47
+ ): VnextArtifactsObservation {
48
+ const root = join(context.projectRoot, ".kxm", "assets");
49
+ const startedAt = new Date().toISOString();
50
+ const startTime = process.hrtime.bigint();
51
+ let checkedCount = 0;
52
+ let failedCount = 0;
53
+
54
+ for (const [index, path] of definition.paths.entries()) {
55
+ checkedCount += 1;
56
+ const result = verifyArtifactExists(root, join(root, path));
57
+ if (!result.ok) failedCount += 1;
58
+ vnextArtifactsGateSeams.afterPathChecked?.({ path, index, checkedCount, failedCount });
59
+ }
60
+
61
+ const elapsedNs = process.hrtime.bigint() - startTime;
62
+ const finishedAt = new Date().toISOString();
63
+ return {
64
+ completeness: "complete",
65
+ spawned: 0,
66
+ pid: null,
67
+ exitCode: null,
68
+ signal: null,
69
+ exitObserved: 0,
70
+ closeObserved: 0,
71
+ stopCause: "none",
72
+ signalsAttempted: "none",
73
+ errorClass: null,
74
+ stdoutSha256: null,
75
+ stdoutBytes: null,
76
+ stdoutComplete: null,
77
+ stderrSha256: null,
78
+ stderrBytes: null,
79
+ stderrComplete: null,
80
+ checkedCount,
81
+ failedCount,
82
+ elapsedMs: Math.max(0, Math.floor(Number(elapsedNs) / 1_000_000)),
83
+ startedAt,
84
+ finishedAt,
85
+ };
86
+ }
@@ -0,0 +1,533 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import type { VnextGateObservationInput } from "./vnext-engine-gate-records.ts";
4
+ import type { VnextGateErrorClass, VnextGateStopCause } from "./vnext-runtime-store.ts";
5
+ import { runtimeError } from "./vnext-runtime-store.ts";
6
+
7
+ const MAX_DIRECT_TIMER_MS = 2_147_483_647;
8
+ export const COMMAND_TERM_GRACE_MS = 2000;
9
+ export const COMMAND_FINAL_WAIT_MS = 2000;
10
+ export const COMMAND_LINGER_MS = 2000;
11
+
12
+ export interface VnextCommandDefinition {
13
+ readonly kind: "command";
14
+ readonly argv: readonly string[];
15
+ readonly timeoutMs: number;
16
+ readonly cwd?: "control";
17
+ }
18
+
19
+ export interface VnextCommandGateSeams {
20
+ timing?: { termGraceMs?: number; finalWaitMs?: number; lingerMs?: number } | undefined;
21
+ beforeSpawn?: ((definition: VnextCommandDefinition) => void) | undefined;
22
+ afterSpawned?: ((pid: number) => void) | undefined;
23
+ afterStopRequested?: ((cause: VnextGateStopCause) => void) | undefined;
24
+ afterChildClose?: (() => void) | undefined;
25
+ }
26
+
27
+ export const vnextCommandGateSeams: VnextCommandGateSeams = {};
28
+
29
+ export type VnextCommandOutcome =
30
+ | { kind: "no-start"; observation: VnextGateObservationInput }
31
+ | { kind: "complete"; observation: VnextGateObservationInput }
32
+ | { kind: "uncertain"; reason: string; observation: VnextGateObservationInput };
33
+
34
+ /**
35
+ * Post-spawn ChildProcess `error` is an uncertain stopCause `error`.
36
+ * The fold reason enum has no post-spawn spawn-error member, so the closed
37
+ * classification keeps errorClass `stream-error` rather than adding a store enum.
38
+ */
39
+ export const POST_SPAWN_CHILD_ERROR_CLASS: VnextGateErrorClass = "stream-error";
40
+
41
+ export interface VnextCommandObserver {
42
+ requestStop(cause: VnextGateStopCause, errorClass?: VnextGateErrorClass): void;
43
+ beginBoundedCleanup(): void;
44
+ run(): Promise<VnextCommandOutcome>;
45
+ }
46
+
47
+ export function createCommandObserver(input: {
48
+ definition: VnextCommandDefinition;
49
+ cwd: string;
50
+ signal: AbortSignal;
51
+ onSpawned: (pid: number) => void;
52
+ }): VnextCommandObserver {
53
+ return new CommandObserver(input);
54
+ }
55
+
56
+ class StreamDigest {
57
+ private readonly hash = createHash("sha256");
58
+ bytes = 0;
59
+ complete: 0 | 1 = 0;
60
+ digest(): string {
61
+ return `sha256:${this.hash.copy().digest("hex")}`;
62
+ }
63
+ write(chunk: Buffer): void {
64
+ this.hash.update(chunk);
65
+ this.bytes += chunk.length;
66
+ }
67
+ end(): void {
68
+ this.complete = 1;
69
+ }
70
+ }
71
+
72
+ class CommandObserver implements VnextCommandObserver {
73
+ private readonly definition: VnextCommandDefinition;
74
+ private readonly cwd: string;
75
+ private readonly signal: AbortSignal;
76
+ private readonly onSpawned: (pid: number) => void;
77
+ private readonly termGraceMs: number;
78
+ private readonly finalWaitMs: number;
79
+ private readonly lingerMs: number;
80
+ private child: ChildProcess | undefined;
81
+ private observedPid: number | undefined;
82
+ private canSignal = true;
83
+ private handedOff = false;
84
+ private cleanupStarted = false;
85
+ private spawnSeen = 0 as 0 | 1;
86
+ private exitObserved = 0 as 0 | 1;
87
+ private closeObserved = 0 as 0 | 1;
88
+ private exitCode: number | null = null;
89
+ private signalName: string | null = null;
90
+ private stopCause: VnextGateStopCause = "none";
91
+ private signalsAttempted: "none" | "term" | "term-kill" = "none";
92
+ private errorClass: VnextGateErrorClass | null = null;
93
+ private startedAt: string;
94
+ private finishedAt: string | null = null;
95
+ private readonly startedMono = process.hrtime.bigint();
96
+ private readonly stdout = new StreamDigest();
97
+ private readonly stderr = new StreamDigest();
98
+ private timeoutTimer: NodeJS.Timeout | undefined;
99
+ private termTimer: NodeJS.Timeout | undefined;
100
+ private finalTimer: NodeJS.Timeout | undefined;
101
+ private lingerTimer: NodeJS.Timeout | undefined;
102
+ private resolveOutcome: ((outcome: VnextCommandOutcome) => void) | undefined;
103
+ private readonly abortListener: () => void;
104
+
105
+ constructor(input: {
106
+ definition: VnextCommandDefinition;
107
+ cwd: string;
108
+ signal: AbortSignal;
109
+ onSpawned: (pid: number) => void;
110
+ }) {
111
+ this.definition = input.definition;
112
+ this.cwd = input.cwd;
113
+ this.signal = input.signal;
114
+ this.onSpawned = input.onSpawned;
115
+ const timing = vnextCommandGateSeams.timing;
116
+ this.termGraceMs = clampTimer(timing?.termGraceMs ?? COMMAND_TERM_GRACE_MS);
117
+ this.finalWaitMs = clampTimer(timing?.finalWaitMs ?? COMMAND_FINAL_WAIT_MS);
118
+ this.lingerMs = clampTimer(timing?.lingerMs ?? COMMAND_LINGER_MS);
119
+ this.startedAt = new Date().toISOString();
120
+ this.abortListener = () => this.requestStop("cancel");
121
+ }
122
+
123
+ requestStop(cause: VnextGateStopCause, errorClass?: VnextGateErrorClass): void {
124
+ if (cause === "none") return;
125
+ if (this.stopCause === "none") {
126
+ this.stopCause = cause;
127
+ if (errorClass && this.errorClass === null) this.errorClass = errorClass;
128
+ vnextCommandGateSeams.afterStopRequested?.(cause);
129
+ this.clearTimer("linger");
130
+ this.beginSignalSequence();
131
+ return;
132
+ }
133
+ if (errorClass && this.errorClass === null) this.errorClass = errorClass;
134
+ }
135
+
136
+ beginBoundedCleanup(): void {
137
+ this.cleanupStarted = true;
138
+ this.requestStop("error", "lost-close");
139
+ }
140
+
141
+ run(): Promise<VnextCommandOutcome> {
142
+ return new Promise((resolve) => {
143
+ this.resolveOutcome = resolve;
144
+ try {
145
+ this.start();
146
+ } catch (error) {
147
+ if (this.child !== undefined || this.spawnSeen === 1 || this.observedPid !== undefined) {
148
+ // A child may already exist; never fabricate no-start after spawn.
149
+ this.requestStop("error", "stop-error");
150
+ return;
151
+ }
152
+ this.finishNoStart("validation", error);
153
+ }
154
+ });
155
+ }
156
+
157
+ private start(): void {
158
+ if (this.signal.aborted || this.cleanupStarted) {
159
+ this.finishNoStart(this.signal.aborted ? "cancel" : "lost-close");
160
+ return;
161
+ }
162
+ try {
163
+ vnextCommandGateSeams.beforeSpawn?.(this.definition);
164
+ } catch {
165
+ this.finishNoStart("validation");
166
+ return;
167
+ }
168
+ if (this.signal.aborted || this.cleanupStarted) {
169
+ this.finishNoStart(this.signal.aborted ? "cancel" : "lost-close");
170
+ return;
171
+ }
172
+ if (
173
+ !Number.isInteger(this.definition.timeoutMs)
174
+ || this.definition.timeoutMs < 1
175
+ || this.definition.timeoutMs > MAX_DIRECT_TIMER_MS
176
+ ) {
177
+ throw runtimeError("run_events_illegal", "command", "command timeoutMs exceeds the direct timer bound");
178
+ }
179
+ this.startedAt = new Date().toISOString();
180
+ let child: ChildProcess;
181
+ try {
182
+ child = spawn(this.definition.argv[0]!, this.definition.argv.slice(1), {
183
+ cwd: this.cwd,
184
+ env: process.env,
185
+ shell: false,
186
+ detached: true,
187
+ stdio: ["ignore", "pipe", "pipe"],
188
+ windowsHide: true,
189
+ });
190
+ } catch {
191
+ this.finishNoStart("validation");
192
+ return;
193
+ }
194
+ this.child = child;
195
+ // child.pid is set synchronously on successful spawn; observe it now so an
196
+ // early stop/cleanup before the async "spawn" event can never be classified
197
+ // as no-start while a live child exists.
198
+ this.observePid(child.pid);
199
+ this.armTimeout();
200
+ this.signal.addEventListener("abort", this.abortListener);
201
+ this.attachChild(child);
202
+ }
203
+
204
+ private attachChild(child: ChildProcess): void {
205
+ child.stdout?.on("data", (chunk: Buffer) => {
206
+ if (this.handedOff) return;
207
+ try {
208
+ this.stdout.write(chunk);
209
+ } catch {
210
+ this.requestStop("error", "stream-error");
211
+ }
212
+ });
213
+ child.stderr?.on("data", (chunk: Buffer) => {
214
+ if (this.handedOff) return;
215
+ try {
216
+ this.stderr.write(chunk);
217
+ } catch {
218
+ this.requestStop("error", "stream-error");
219
+ }
220
+ });
221
+ child.stdout?.on("end", () => {
222
+ if (this.handedOff) return;
223
+ try {
224
+ this.stdout.end();
225
+ } catch {
226
+ this.requestStop("error", "stream-error");
227
+ }
228
+ });
229
+ child.stderr?.on("end", () => {
230
+ if (this.handedOff) return;
231
+ try {
232
+ this.stderr.end();
233
+ } catch {
234
+ this.requestStop("error", "stream-error");
235
+ }
236
+ });
237
+ child.stdout?.on("error", () => this.requestStop("error", "stream-error"));
238
+ child.stderr?.on("error", () => this.requestStop("error", "stream-error"));
239
+ child.once("spawn", () => {
240
+ this.spawnSeen = 1;
241
+ this.observePid(child.pid);
242
+ if (this.observedPid !== undefined) {
243
+ this.notifySpawned();
244
+ } else {
245
+ this.requestStop("error", POST_SPAWN_CHILD_ERROR_CLASS);
246
+ }
247
+ });
248
+ child.once("error", () => {
249
+ if (this.spawnSeen === 0 && child.pid == null && this.observedPid === undefined) {
250
+ this.finishNoStart("spawn-error");
251
+ return;
252
+ }
253
+ if (this.spawnSeen === 0) {
254
+ this.spawnSeen = 1;
255
+ this.observePid(child.pid);
256
+ this.notifySpawned();
257
+ }
258
+ this.requestStop("error", POST_SPAWN_CHILD_ERROR_CLASS);
259
+ });
260
+ child.once("exit", (code, signal) => {
261
+ this.exitObserved = 1;
262
+ this.exitCode = code;
263
+ this.signalName = signal;
264
+ if (this.closeObserved === 1) {
265
+ this.handoff();
266
+ return;
267
+ }
268
+ this.armLinger();
269
+ });
270
+ child.once("close", () => {
271
+ this.closeObserved = 1;
272
+ this.finishedAt = new Date().toISOString();
273
+ this.canSignal = false;
274
+ this.clearTimer("term");
275
+ this.clearTimer("final");
276
+ this.clearTimer("linger");
277
+ this.clearTimer("timeout");
278
+ child.stdout?.destroy();
279
+ child.stderr?.destroy();
280
+ dropVnextCommandStopCapability(this);
281
+ try {
282
+ vnextCommandGateSeams.afterChildClose?.();
283
+ } catch {
284
+ // test seam
285
+ }
286
+ this.handoff();
287
+ });
288
+ }
289
+
290
+ private notifiedSpawned = false;
291
+
292
+ private observePid(pid: number | undefined): void {
293
+ if (this.observedPid === undefined && typeof pid === "number" && pid > 0) {
294
+ this.observedPid = pid;
295
+ }
296
+ }
297
+
298
+ private notifySpawned(): void {
299
+ if (this.notifiedSpawned || this.observedPid === undefined) return;
300
+ this.notifiedSpawned = true;
301
+ try {
302
+ this.onSpawned(this.observedPid);
303
+ } catch {
304
+ this.requestStop("error", "recording-error");
305
+ }
306
+ try {
307
+ vnextCommandGateSeams.afterSpawned?.(this.observedPid);
308
+ } catch {
309
+ this.requestStop("error", "recording-error");
310
+ }
311
+ }
312
+
313
+ private armTimeout(): void {
314
+ this.timeoutTimer = schedule(this.definition.timeoutMs, () => {
315
+ this.requestStop("timeout");
316
+ });
317
+ }
318
+
319
+ private armLinger(): void {
320
+ if (this.stopCause !== "none") return;
321
+ if (this.lingerTimer || this.closeObserved === 1) return;
322
+ this.lingerTimer = schedule(this.lingerMs, () => {
323
+ this.requestStop("error", "lost-close");
324
+ });
325
+ }
326
+
327
+ private beginSignalSequence(): void {
328
+ if (this.closeObserved === 1 || this.handedOff) {
329
+ this.handoff();
330
+ return;
331
+ }
332
+ const pid = this.observedPid;
333
+ if (!this.canSignal || pid === undefined) {
334
+ this.armFinal();
335
+ return;
336
+ }
337
+ this.signalGroup(pid, "SIGTERM", "term");
338
+ this.termTimer = schedule(this.termGraceMs, () => {
339
+ if (this.closeObserved === 1 || this.handedOff) return;
340
+ if (!this.canSignal || this.observedPid === undefined) {
341
+ this.armFinal();
342
+ return;
343
+ }
344
+ this.signalGroup(this.observedPid, "SIGKILL", "term-kill");
345
+ this.armFinal();
346
+ });
347
+ }
348
+
349
+ private signalGroup(pid: number, signal: NodeJS.Signals, attempted: "term" | "term-kill"): void {
350
+ if (!this.canSignal || this.closeObserved === 1 || pid <= 0) return;
351
+ try {
352
+ process.kill(-pid, signal);
353
+ this.signalsAttempted = attempted;
354
+ } catch (error) {
355
+ const code = (error as NodeJS.ErrnoException).code;
356
+ if (code === "ESRCH") {
357
+ this.canSignal = false;
358
+ return;
359
+ }
360
+ if (this.errorClass === null) this.errorClass = "stop-error";
361
+ }
362
+ }
363
+
364
+ private armFinal(): void {
365
+ if (this.finalTimer) return;
366
+ this.finalTimer = schedule(this.finalWaitMs, () => {
367
+ this.canSignal = false;
368
+ this.handoff();
369
+ });
370
+ }
371
+
372
+ private finishNoStart(kind: "validation" | "spawn-error" | "cancel" | "lost-close", _error?: unknown): void {
373
+ if (this.handedOff) return;
374
+ if (kind === "lost-close" && this.spawnSeen === 1) {
375
+ this.requestStop("error", "lost-close");
376
+ return;
377
+ }
378
+ const now = new Date().toISOString();
379
+ const cancel = kind === "cancel";
380
+ this.handoffWith({
381
+ kind: "no-start",
382
+ observation: this.baseObservation({
383
+ completeness: "no-start",
384
+ spawned: 0,
385
+ pid: null,
386
+ exitCode: null,
387
+ signal: null,
388
+ exitObserved: 0,
389
+ closeObserved: 0,
390
+ stopCause: cancel ? "cancel" : "none",
391
+ signalsAttempted: "none",
392
+ errorClass: cancel ? null : kind === "spawn-error" ? "spawn-error" : "validation",
393
+ stdoutSha256: null,
394
+ stdoutBytes: null,
395
+ stdoutComplete: null,
396
+ stderrSha256: null,
397
+ stderrBytes: null,
398
+ stderrComplete: null,
399
+ finishedAt: now,
400
+ }),
401
+ });
402
+ }
403
+
404
+ private handoff(): void {
405
+ if (this.handedOff) return;
406
+ if (this.spawnSeen === 0 && this.observedPid === undefined) {
407
+ if (this.stopCause === "cancel" || this.signal.aborted) {
408
+ this.finishNoStart("cancel");
409
+ return;
410
+ }
411
+ if (this.errorClass === "spawn-error") {
412
+ this.finishNoStart("spawn-error");
413
+ return;
414
+ }
415
+ this.finishNoStart("validation");
416
+ return;
417
+ }
418
+ const observation = this.snapshot();
419
+ if (isCompleteNormal(observation)) {
420
+ this.handoffWith({ kind: "complete", observation });
421
+ return;
422
+ }
423
+ this.handoffWith({
424
+ kind: "uncertain",
425
+ reason: this.uncertaintyReason(observation),
426
+ observation: { ...observation, completeness: "incomplete" },
427
+ });
428
+ }
429
+
430
+ private uncertaintyReason(observation: VnextGateObservationInput): string {
431
+ // Requested primary cause is independent of the later observed exit signal.
432
+ // Signal-termination is only for an externally signalled exit with no earlier request.
433
+ if (this.stopCause === "timeout") return "timeout";
434
+ if (this.stopCause === "cancel") return "cancel";
435
+ if (this.errorClass === "recording-error") return "recording-error";
436
+ if (this.errorClass === "lost-close" || (this.exitObserved === 1 && this.closeObserved === 0)) return "lost-close";
437
+ if (this.errorClass === "stop-error") return "stop-error";
438
+ if (this.errorClass === "stream-error") return "stream-error";
439
+ if (this.stopCause === "error") return "stream-error";
440
+ if (observation.signal !== null) return "signal-termination";
441
+ return "stop-error";
442
+ }
443
+
444
+ private snapshot(): VnextGateObservationInput {
445
+ return this.baseObservation({
446
+ completeness: "complete",
447
+ spawned: this.spawnSeen,
448
+ pid: this.observedPid ?? null,
449
+ exitCode: this.signalName !== null ? null : this.exitCode,
450
+ signal: this.signalName,
451
+ exitObserved: this.exitObserved,
452
+ closeObserved: this.closeObserved,
453
+ stopCause: this.stopCause,
454
+ signalsAttempted: this.signalsAttempted,
455
+ errorClass: this.errorClass,
456
+ stdoutSha256: this.stdout.digest(),
457
+ stdoutBytes: this.stdout.bytes,
458
+ stdoutComplete: this.stdout.complete,
459
+ stderrSha256: this.stderr.digest(),
460
+ stderrBytes: this.stderr.bytes,
461
+ stderrComplete: this.stderr.complete,
462
+ finishedAt: this.finishedAt,
463
+ });
464
+ }
465
+
466
+ private baseObservation(
467
+ facts: Omit<VnextGateObservationInput, "checkedCount" | "failedCount" | "elapsedMs" | "startedAt"> & {
468
+ elapsedMs?: number;
469
+ startedAt?: string;
470
+ },
471
+ ): VnextGateObservationInput {
472
+ const elapsedNs = process.hrtime.bigint() - this.startedMono;
473
+ return {
474
+ ...facts,
475
+ checkedCount: null,
476
+ failedCount: null,
477
+ elapsedMs: facts.elapsedMs ?? Math.max(0, Math.floor(Number(elapsedNs) / 1_000_000)),
478
+ startedAt: facts.startedAt ?? this.startedAt,
479
+ };
480
+ }
481
+
482
+ private handoffWith(outcome: VnextCommandOutcome): void {
483
+ if (this.handedOff) return;
484
+ this.handedOff = true;
485
+ this.canSignal = false;
486
+ this.signal.removeEventListener("abort", this.abortListener);
487
+ this.clearTimer("timeout");
488
+ this.clearTimer("term");
489
+ this.clearTimer("final");
490
+ this.clearTimer("linger");
491
+ this.child?.unref();
492
+ dropVnextCommandStopCapability(this);
493
+ this.resolveOutcome?.(outcome);
494
+ }
495
+
496
+ private clearTimer(name: "timeout" | "term" | "final" | "linger"): void {
497
+ const current = this[`${name}Timer`];
498
+ if (current) clearTimeout(current);
499
+ this[`${name}Timer`] = undefined;
500
+ }
501
+ }
502
+
503
+ function dropVnextCommandStopCapability(observer: CommandObserver): void {
504
+ observer.requestStop = () => undefined;
505
+ }
506
+
507
+ function isCompleteNormal(observation: VnextGateObservationInput): boolean {
508
+ return observation.completeness === "complete"
509
+ && observation.spawned === 1
510
+ && typeof observation.pid === "number"
511
+ && typeof observation.exitCode === "number"
512
+ && observation.signal === null
513
+ && observation.exitObserved === 1
514
+ && observation.closeObserved === 1
515
+ && observation.stopCause === "none"
516
+ && observation.signalsAttempted === "none"
517
+ && observation.errorClass === null
518
+ && observation.stdoutComplete === 1
519
+ && observation.stderrComplete === 1
520
+ && typeof observation.stdoutSha256 === "string"
521
+ && typeof observation.stderrSha256 === "string";
522
+ }
523
+
524
+ function clampTimer(ms: number): number {
525
+ if (!Number.isInteger(ms) || ms < 0 || ms > MAX_DIRECT_TIMER_MS) {
526
+ throw runtimeError("run_events_illegal", "command", "command timer is outside the direct timer bound");
527
+ }
528
+ return ms;
529
+ }
530
+
531
+ function schedule(ms: number, fn: () => void): NodeJS.Timeout {
532
+ return setTimeout(fn, clampTimer(ms));
533
+ }