@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,479 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
5
+ import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { createServer } from "node:net";
7
+ import { tmpdir } from "node:os";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+
11
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const hubScript = join(root, "scripts", "kxm-hub.mjs");
13
+ const workerScript = join(root, "scripts", "kxm-worker.mjs");
14
+ const extensionPath = join(root, "plugins", "kxm", "src", "extension.ts");
15
+ const DEFAULT_TIMEOUT_MS = 120_000;
16
+ const MAX_CAPTURE_CHARS = 32_768;
17
+
18
+ export function parseSmokeModels(value) {
19
+ const models = [...new Set(String(value ?? "").split(",").map((item) => item.trim()).filter(Boolean))];
20
+ if (models.some((model) => !/^[A-Za-z0-9._:/-]{1,160}$/.test(model))) {
21
+ throw new Error("KXM_SMOKE_MODELS contains an invalid model ID");
22
+ }
23
+ return models;
24
+ }
25
+
26
+ export function boundedSafeMessage(value, secrets = []) {
27
+ let result = value instanceof Error ? value.message : String(value);
28
+ for (const secret of secrets) {
29
+ if (secret) result = result.replaceAll(secret, "[REDACTED]");
30
+ }
31
+ result = result.replace(/(?:sk|key|token|secret)[-_A-Za-z0-9]{12,}/gi, "[REDACTED]");
32
+ return result.slice(0, 500);
33
+ }
34
+
35
+ export async function runSmokePhases(phases, cleanup) {
36
+ const completed = [];
37
+ try {
38
+ const values = {};
39
+ for (const [name, action] of phases) {
40
+ values[name] = await action(values);
41
+ completed.push(name);
42
+ }
43
+ return { values, completed };
44
+ } finally {
45
+ await cleanup();
46
+ }
47
+ }
48
+
49
+ function writeOutcome(outcome) {
50
+ process.stdout.write(`${JSON.stringify(outcome)}\n`);
51
+ }
52
+
53
+ function runCommand(command, args, options = {}) {
54
+ return spawnSync(command, args, {
55
+ encoding: "utf8",
56
+ windowsHide: true,
57
+ shell: process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command),
58
+ timeout: options.timeout ?? 15_000,
59
+ env: options.env ?? process.env,
60
+ stdio: ["ignore", "pipe", "pipe"],
61
+ });
62
+ }
63
+
64
+ function resolvePiCommand() {
65
+ const configured = process.env.KXM_SMOKE_PI_COMMAND?.trim();
66
+ if (configured) return configured;
67
+ const locator = runCommand(process.platform === "win32" ? "where.exe" : "which", ["pi"]);
68
+ if (locator.status !== 0) return undefined;
69
+ const candidates = String(locator.stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
70
+ return process.platform === "win32"
71
+ ? candidates.find((candidate) => /\.cmd$/i.test(candidate)) ?? candidates[0]
72
+ : candidates[0];
73
+ }
74
+
75
+ function modelAuthReady(piCommand, model) {
76
+ const checked = runCommand(piCommand, ["auth", "check", "--model", model, "--json", "--no-refresh"]);
77
+ if (checked.status !== 0) return false;
78
+ try {
79
+ return JSON.parse(String(checked.stdout)).status === "ready";
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ async function freePort() {
86
+ const server = createServer();
87
+ await new Promise((accept, reject) => {
88
+ server.once("error", reject);
89
+ server.listen(0, "127.0.0.1", accept);
90
+ });
91
+ const address = server.address();
92
+ if (!address || typeof address === "string") throw new Error("failed to allocate a local smoke port");
93
+ await new Promise((accept, reject) => server.close((error) => error ? reject(error) : accept()));
94
+ return address.port;
95
+ }
96
+
97
+ function captureProcess(child, label) {
98
+ const state = { label, stdout: "", stderr: "", exit: undefined };
99
+ child.stdout?.on("data", (chunk) => {
100
+ state.stdout = `${state.stdout}${chunk}`.slice(-MAX_CAPTURE_CHARS);
101
+ });
102
+ child.stderr?.on("data", (chunk) => {
103
+ state.stderr = `${state.stderr}${chunk}`.slice(-MAX_CAPTURE_CHARS);
104
+ });
105
+ child.once("exit", (code, signal) => {
106
+ state.exit = { code, signal };
107
+ });
108
+ return state;
109
+ }
110
+
111
+ function spawnNode(script, env, label) {
112
+ const child = spawn(process.execPath, [script], {
113
+ cwd: root,
114
+ env,
115
+ stdio: ["ignore", "pipe", "pipe"],
116
+ windowsHide: true,
117
+ });
118
+ return { child, capture: captureProcess(child, label) };
119
+ }
120
+
121
+ async function waitFor(check, timeoutMs, label, intervalMs = 250) {
122
+ const deadline = Date.now() + timeoutMs;
123
+ let lastError;
124
+ while (Date.now() < deadline) {
125
+ try {
126
+ const value = await check();
127
+ if (value) return value;
128
+ } catch (error) {
129
+ lastError = error;
130
+ }
131
+ await new Promise((accept) => setTimeout(accept, intervalMs));
132
+ }
133
+ throw new Error(`${label} timed out${lastError ? `: ${boundedSafeMessage(lastError)}` : ""}`);
134
+ }
135
+
136
+ async function api(baseUrl, path, options = {}) {
137
+ const response = await fetch(`${baseUrl}${path}`, {
138
+ method: options.method ?? "GET",
139
+ headers: {
140
+ "content-type": "application/json",
141
+ authorization: `Bearer ${options.authToken}`,
142
+ ...(options.identity ? {
143
+ "x-kxm-agent-id": options.identity.agent.id,
144
+ "x-kxm-agent-key": options.identity.agentKey,
145
+ } : {}),
146
+ ...(options.headers ?? {}),
147
+ },
148
+ ...(options.body === undefined ? {} : { body: typeof options.body === "string" ? options.body : JSON.stringify(options.body) }),
149
+ signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),
150
+ });
151
+ const text = await response.text();
152
+ let body = {};
153
+ if (text) {
154
+ try {
155
+ body = JSON.parse(text);
156
+ } catch {
157
+ throw new Error(`hub returned invalid JSON with HTTP ${response.status}`);
158
+ }
159
+ }
160
+ if (!response.ok) throw new Error(`hub HTTP ${response.status}: ${body.code ?? "request_failed"}`);
161
+ return body;
162
+ }
163
+
164
+ async function registerOperator(baseUrl, authToken, project) {
165
+ return await api(baseUrl, "/v1/agents/register", {
166
+ method: "POST",
167
+ authToken,
168
+ body: { name: "smoke-operator", purpose: "bounded smoke assertions", project },
169
+ });
170
+ }
171
+
172
+ async function agents(baseUrl, authToken, operator) {
173
+ const result = await api(baseUrl, "/v1/agents", { authToken, identity: operator });
174
+ return result.agents;
175
+ }
176
+
177
+ export function buildSmokeRequest(target, content, idempotencyKey) {
178
+ return { target, content, delivery: "followUp", idempotencyKey };
179
+ }
180
+
181
+ async function sendAndAwait(baseUrl, authToken, operator, target, content, timeoutMs, idempotencyKey) {
182
+ const sent = await api(baseUrl, "/v1/messages", {
183
+ method: "POST",
184
+ authToken,
185
+ identity: operator,
186
+ body: buildSmokeRequest(target, content, idempotencyKey),
187
+ });
188
+ return await waitFor(async () => {
189
+ const result = await api(baseUrl, `/v1/messages/${encodeURIComponent(sent.message.id)}`, {
190
+ authToken,
191
+ identity: operator,
192
+ });
193
+ if (["cancelled", "expired", "error"].includes(result.message.status)) {
194
+ throw new Error(`peer request ended as ${result.message.status}`);
195
+ }
196
+ return result.message.status === "replied" && typeof result.message.reply?.content === "string"
197
+ && result.message.reply.content.trim() ? result.message : undefined;
198
+ }, timeoutMs, `reply from ${target}`, 500);
199
+ }
200
+
201
+ function createPiShim(tempRoot, piCommand) {
202
+ if (process.platform === "win32") {
203
+ const commandShim = join(tempRoot, "pi-smoke-shim.cmd");
204
+ const quoteBatch = (value) => `"${String(value).replaceAll('"', '""')}"`;
205
+ writeFileSync(commandShim, [
206
+ "@echo off",
207
+ `call ${quoteBatch(piCommand)} --extension ${quoteBatch(extensionPath)} --approve --no-context-files --no-skills --no-prompt-templates --no-themes %*`,
208
+ "exit /b %errorlevel%",
209
+ "",
210
+ ].join("\r\n"), "utf8");
211
+ return commandShim;
212
+ }
213
+ const shim = join(tempRoot, "pi-smoke-shim.mjs");
214
+ writeFileSync(shim, [
215
+ "#!/usr/bin/env node",
216
+ 'import { spawn } from "node:child_process";',
217
+ `const command = ${JSON.stringify(piCommand)};`,
218
+ `const prefix = ${JSON.stringify(["--extension", extensionPath, "--approve", "--no-context-files", "--no-skills", "--no-prompt-templates", "--no-themes"])};`,
219
+ "const windowsScript = process.platform === 'win32' && /\\.(?:cmd|bat)$/i.test(command);",
220
+ "const quote = (value) => `\"${String(value).replaceAll('\\\"', '\\\"\\\"')}\"`;",
221
+ "const executable = windowsScript ? process.env.ComSpec || 'cmd.exe' : command;",
222
+ "const args = [...prefix, ...process.argv.slice(2)];",
223
+ "const launchArgs = windowsScript ? ['/d', '/s', '/v:off', '/c', `\"${[command, ...args].map(quote).join(' ')}\"`] : args;",
224
+ "const child = spawn(executable, launchArgs, { stdio: 'inherit', windowsHide: true });",
225
+ "child.once('error', (error) => { process.stderr.write(`${error.message}\\n`); process.exit(1); });",
226
+ "child.once('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 1); });",
227
+ "for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => child.kill(signal));",
228
+ ].join("\n"), { encoding: "utf8", mode: 0o700 });
229
+ chmodSync(shim, 0o700);
230
+ return shim;
231
+ }
232
+
233
+ function readPidRecord(path) {
234
+ try {
235
+ const value = JSON.parse(readFileSync(path, "utf8"));
236
+ return Number.isInteger(value.pid) && typeof value.startedAt === "string" && typeof value.controlFile === "string"
237
+ ? value : undefined;
238
+ } catch {
239
+ return undefined;
240
+ }
241
+ }
242
+
243
+ function requestGracefulStop(stateDir, role, name, project) {
244
+ const safeName = name?.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 32) || "agent";
245
+ const safeProject = project?.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 24) || "project";
246
+ const digest = name && project
247
+ ? createHash("sha256").update(JSON.stringify({ project, agentName: name })).digest("hex").slice(0, 24)
248
+ : undefined;
249
+ const pidPath = join(stateDir, role === "hub" ? "hub.pid" : `worker-${safeProject}-${safeName}-${digest}.pid`);
250
+ const record = readPidRecord(pidPath);
251
+ if (!record) return false;
252
+ writeFileSync(join(stateDir, record.controlFile), `${JSON.stringify({ version: 1, startedAt: record.startedAt, ...(record.generation ? { generation: record.generation } : {}), requestedAt: new Date().toISOString() })}\n`, {
253
+ encoding: "utf8",
254
+ mode: 0o600,
255
+ });
256
+ return true;
257
+ }
258
+
259
+ async function waitForExit(processRecord, timeoutMs) {
260
+ if (processRecord.capture.exit) return;
261
+ await Promise.race([
262
+ new Promise((accept) => processRecord.child.once("exit", accept)),
263
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${processRecord.capture.label} did not stop gracefully`)), timeoutMs)),
264
+ ]);
265
+ }
266
+
267
+ function forceTreeStop(child) {
268
+ if (!child || child.exitCode !== null) return;
269
+ if (process.platform === "win32" && child.pid) {
270
+ spawnSync("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" });
271
+ } else {
272
+ child.kill("SIGKILL");
273
+ }
274
+ }
275
+
276
+ async function stopRecord(processRecord, stateDir, role, name, project) {
277
+ if (!processRecord || processRecord.capture.exit) return;
278
+ requestGracefulStop(stateDir, role, name, project);
279
+ try {
280
+ await waitForExit(processRecord, 20_000);
281
+ } catch {
282
+ forceTreeStop(processRecord.child);
283
+ await waitForExit(processRecord, 5_000).catch(() => undefined);
284
+ }
285
+ }
286
+
287
+ export async function runRealSmoke(options = {}) {
288
+ const models = options.models ?? parseSmokeModels(process.env.KXM_SMOKE_MODELS);
289
+ if (models.length < 2) throw new Error("KXM_SMOKE_MODELS must name at least two distinct models");
290
+ const piCommand = options.piCommand ?? resolvePiCommand();
291
+ if (!piCommand) return { ok: true, skipped: true, reason: "real Pi binary unavailable" };
292
+ const probe = runCommand(piCommand, ["--version"]);
293
+ if (probe.status !== 0) return { ok: true, skipped: true, reason: "real Pi binary unavailable" };
294
+ const unavailableModels = models.filter((model) => !modelAuthReady(piCommand, model));
295
+ if (unavailableModels.length > 0) {
296
+ return { ok: true, skipped: true, reason: "model credentials unavailable", unavailableModels };
297
+ }
298
+
299
+ const timeoutMs = Number(process.env.KXM_SMOKE_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS);
300
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 30_000 || timeoutMs > 600_000) {
301
+ throw new Error("KXM_SMOKE_TIMEOUT_MS must be an integer from 30000 to 600000");
302
+ }
303
+ const tempRoot = mkdtempSync(join(tmpdir(), "pi-mesh-real-smoke-"));
304
+ const workspaceDir = join(tempRoot, ".kxm");
305
+ const stateDir = join(workspaceDir, "state");
306
+ const logsDir = join(workspaceDir, "logs");
307
+ const assetsDir = join(workspaceDir, "assets");
308
+ for (const directory of [workspaceDir, stateDir, logsDir, assetsDir]) mkdirSync(directory, { recursive: true });
309
+ const port = await freePort();
310
+ const baseUrl = `http://127.0.0.1:${port}`;
311
+ const project = `smoke-${randomUUID()}`;
312
+ const authToken = randomBytes(32).toString("hex");
313
+ const webhookSecret = randomBytes(32).toString("hex");
314
+ const shim = createPiShim(tempRoot, piCommand);
315
+ const workerNames = ["smoke-alpha", "smoke-beta"];
316
+ const workflow = [{
317
+ id: "real-pi-smoke",
318
+ source: "generic",
319
+ project,
320
+ target: "smoke-operator",
321
+ secretEnv: "KXM_SMOKE_WEBHOOK_SECRET",
322
+ event: "smoke.requested",
323
+ delivery: "followUp",
324
+ promptTemplate: "Run isolated smoke {{runId}}",
325
+ stages: [{ id: "verify", label: "Verify", instructions: "Record the deterministic smoke result", requiredEvidence: ["real Pi evidence"], maxAttempts: 1 }],
326
+ }];
327
+ const commonEnv = {
328
+ ...process.env,
329
+ KXM_HOST: "127.0.0.1",
330
+ KXM_PORT: String(port),
331
+ KXM_SERVER_URL: baseUrl,
332
+ KXM_AUTH_TOKEN: authToken,
333
+ KXM_PROJECT: project,
334
+ KXM_WORKDIR: tempRoot,
335
+ KXM_WORKSPACE_DIR: workspaceDir,
336
+ KXM_CONFIG_DIR: join(workspaceDir, "config"),
337
+ KXM_LOGS_DIR: logsDir,
338
+ KXM_ASSETS_DIR: assetsDir,
339
+ KXM_STATE_DIR: stateDir,
340
+ KXM_DATA_PATH: join(stateDir, "kxm.db"),
341
+ KXM_WEBHOOK_WORKFLOWS: JSON.stringify(workflow),
342
+ KXM_SMOKE_WEBHOOK_SECRET: webhookSecret,
343
+ };
344
+ const processes = { hub: undefined, workers: new Map() };
345
+ let stage = "start";
346
+ try {
347
+ const startHub = () => {
348
+ const record = spawnNode(hubScript, commonEnv, "hub");
349
+ processes.hub = record;
350
+ return record;
351
+ };
352
+ const startWorker = (index) => {
353
+ const name = workerNames[index];
354
+ const record = spawnNode(workerScript, {
355
+ ...commonEnv,
356
+ KXM_AGENT_NAME: name,
357
+ KXM_AGENT_PURPOSE: "real Pi smoke peer",
358
+ KXM_WORKER_MODEL: models[index],
359
+ KXM_PI_COMMAND: shim,
360
+ KXM_WORKER_DRAIN_MS: "10000",
361
+ KXM_WORKER_MAX_RESTARTS: "2",
362
+ KXM_WORKER_SESSION_ISOLATION: "workflow",
363
+ }, name);
364
+ processes.workers.set(name, record);
365
+ return record;
366
+ };
367
+ await runSmokePhases([
368
+ ["hub-ready", async () => {
369
+ stage = "hub-ready";
370
+ startHub();
371
+ await waitFor(async () => {
372
+ const response = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(1_000) });
373
+ return response.ok;
374
+ }, 20_000, "hub readiness");
375
+ return registerOperator(baseUrl, authToken, project);
376
+ }],
377
+ ["discovery", async (values) => {
378
+ stage = "discovery";
379
+ const operator = values["hub-ready"];
380
+ startWorker(0);
381
+ startWorker(1);
382
+ const discovered = await waitFor(async () => {
383
+ const current = await agents(baseUrl, authToken, operator);
384
+ return workerNames.every((name) => current.some((agent) => agent.name === name)) ? current : undefined;
385
+ }, timeoutMs, "two real Pi workers");
386
+ return { operator, originalIds: Object.fromEntries(workerNames.map((name) => [name, discovered.find((agent) => agent.name === name).id])) };
387
+ }],
388
+ ["request-reply", async (values) => {
389
+ stage = "request-reply";
390
+ const { operator } = values.discovery;
391
+ return sendAndAwait(baseUrl, authToken, operator, workerNames[0], "This is a transport smoke check. Reply with a short acknowledgement and do not call tools.", timeoutMs, `single-${randomUUID()}`);
392
+ }],
393
+ ["fanout", async (values) => {
394
+ stage = "fanout";
395
+ const { operator } = values.discovery;
396
+ const replies = await Promise.all(workerNames.map((name) => sendAndAwait(baseUrl, authToken, operator, name, "Independently confirm the transport smoke check in one short sentence. Do not call tools.", timeoutMs, `fanout-${name}-${randomUUID()}`)));
397
+ if (replies.length !== 2) throw new Error("fanout did not return two replies");
398
+ return replies;
399
+ }],
400
+ ["durable-restart-resume", async (values) => {
401
+ stage = "durable-restart-resume";
402
+ const { originalIds } = values.discovery;
403
+ await stopRecord(processes.workers.get(workerNames[0]), stateDir, "worker", workerNames[0], project);
404
+ await stopRecord(processes.hub, stateDir, "hub");
405
+ startHub();
406
+ await waitFor(async () => {
407
+ const response = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(1_000) });
408
+ return response.ok;
409
+ }, 20_000, "restarted hub readiness");
410
+ const resumedOperator = await registerOperator(baseUrl, authToken, project);
411
+ startWorker(0);
412
+ const resumed = await waitFor(async () => {
413
+ const current = await agents(baseUrl, authToken, resumedOperator);
414
+ return workerNames.every((name) => current.some((agent) => agent.name === name)) ? current : undefined;
415
+ }, timeoutMs, "worker resume");
416
+ for (const name of workerNames) {
417
+ if (resumed.find((agent) => agent.name === name).id !== originalIds[name]) throw new Error(`durable identity changed for ${name}`);
418
+ }
419
+ await sendAndAwait(baseUrl, authToken, resumedOperator, workerNames[0], "Confirm transport still works after the durable restart. Reply briefly and do not call tools.", timeoutMs, `post-restart-${randomUUID()}`);
420
+ return resumedOperator;
421
+ }],
422
+ ["journal-checkpoint", async (values) => {
423
+ stage = "workflow-journal-checkpoint";
424
+ const resumedOperator = values["durable-restart-resume"];
425
+ const payload = JSON.stringify({ event: "smoke.requested" });
426
+ const started = await api(baseUrl, "/v1/webhooks/real-pi-smoke", {
427
+ method: "POST",
428
+ authToken,
429
+ headers: { "x-mesh-event": "smoke.requested", "x-kxm-delivery-id": `smoke-${randomUUID()}`, "x-hub-signature": `sha256=${createHmac("sha256", webhookSecret).update(payload).digest("hex")}` },
430
+ body: payload,
431
+ });
432
+ await api(baseUrl, `/v1/workflows/${encodeURIComponent(started.run.id)}/journal`, {
433
+ method: "POST",
434
+ authToken,
435
+ identity: resumedOperator,
436
+ body: { category: "lesson", area: "harness", severity: "info", summary: "Real Pi smoke transport completed", evidence: ["discovery", "request-reply", "fanout", "post-restart-request-reply"] },
437
+ });
438
+ const checkpoint = await api(baseUrl, `/v1/workflows/${encodeURIComponent(started.run.id)}/checkpoints`, {
439
+ method: "POST",
440
+ authToken,
441
+ identity: resumedOperator,
442
+ body: { stageId: "verify", status: "passed", summary: "All real Pi smoke assertions passed", evidence: { "real Pi evidence": "discovery, request/reply, fanout, and post-restart request/reply passed" } },
443
+ });
444
+ if (!checkpoint.completed) throw new Error("workflow checkpoint did not complete");
445
+ const recorded = await api(baseUrl, `/v1/workflows/${encodeURIComponent(started.run.id)}`, { authToken, identity: resumedOperator });
446
+ if (recorded.run.status !== "completed"
447
+ || recorded.run.stages[0]?.status !== "passed"
448
+ || !recorded.journal.some((entry) => entry.category === "lesson" && entry.area === "harness")) {
449
+ throw new Error("workflow journal/checkpoint was not durable");
450
+ }
451
+ }],
452
+ ], async () => {
453
+ for (const [name, record] of processes.workers) await stopRecord(record, stateDir, "worker", name, project);
454
+ await stopRecord(processes.hub, stateDir, "hub");
455
+ for (const record of processes.workers.values()) forceTreeStop(record.child);
456
+ forceTreeStop(processes.hub?.child);
457
+ rmSync(tempRoot, { recursive: true, force: true });
458
+ });
459
+ return { ok: true, skipped: false, realPiWorkers: 2, assertions: ["discovery", "request-reply", "fanout", "durable-restart-resume", "post-restart-request-reply", "journal", "checkpoint"] };
460
+ } catch (error) {
461
+ throw new Error(`${stage}: ${boundedSafeMessage(error, [authToken, webhookSecret])}`);
462
+ }
463
+ }
464
+
465
+ async function main() {
466
+ const enabled = process.env.KXM_SMOKE === "1" || process.argv.includes("--real-pi");
467
+ if (!enabled) {
468
+ writeOutcome({ ok: true, skipped: true, reason: "KXM_SMOKE is not 1" });
469
+ return;
470
+ }
471
+ try {
472
+ writeOutcome(await runRealSmoke());
473
+ } catch (error) {
474
+ writeOutcome({ ok: false, skipped: false, error: boundedSafeMessage(error) });
475
+ process.exitCode = 1;
476
+ }
477
+ }
478
+
479
+ if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) await main();