@kontextmind/kxm 0.7.40 → 0.7.44

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 (178) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +45 -7
  3. package/docs/README.md +5 -3
  4. package/docs/agent-skills.md +1 -1
  5. package/docs/architecture.md +1 -1
  6. package/docs/configuration.md +28 -5
  7. package/docs/{vnext → contracts}/README.md +7 -7
  8. package/docs/{vnext → contracts}/architecture.md +2 -2
  9. package/docs/{vnext → contracts}/migration.md +25 -25
  10. package/docs/{vnext → contracts}/routing.md +2 -2
  11. package/docs/{vnext → contracts}/synchronization.md +1 -1
  12. package/docs/{vnext → contracts}/validation.md +7 -7
  13. package/docs/getting-started.md +1 -1
  14. package/docs/kb/qa-authentik-authentication.md +47 -0
  15. package/docs/kb/qa-extension-install-and-hub-bootstrap.md +68 -0
  16. package/docs/kb/qa-hub-on-a-public-host.md +31 -0
  17. package/docs/kb/qa-sqlite-vs-duckdb.md +18 -0
  18. package/docs/kb/qa-what-the-hub-stores.md +47 -0
  19. package/docs/packages.md +91 -0
  20. package/docs/templates/architecture.md +1 -1
  21. package/docs/test-matrix.md +6 -6
  22. package/docs/tui-components.md +131 -0
  23. package/examples/README.md +2 -2
  24. package/examples/{vnext → project}/.kxm/agents/critic-3.yaml +1 -1
  25. package/examples/{vnext → project}/README.md +4 -4
  26. package/package.json +18 -8
  27. package/packages/core/tui/CHANGELOG.md +19 -0
  28. package/packages/core/tui/LICENSE +21 -0
  29. package/packages/core/tui/README.md +45 -0
  30. package/packages/core/tui/dist/index.js +1439 -0
  31. package/packages/core/tui/src/adapters/pi.ts +73 -0
  32. package/packages/core/tui/src/adapters/terminal.ts +106 -0
  33. package/packages/core/tui/src/exports/index.ts +30 -0
  34. package/packages/core/tui/src/services/registry.ts +274 -0
  35. package/packages/core/tui/src/tui/keys.ts +130 -0
  36. package/packages/core/tui/src/tui/layout.ts +117 -0
  37. package/packages/core/tui/src/tui/panel.ts +435 -0
  38. package/packages/core/tui/src/tui/panelComponent.ts +208 -0
  39. package/packages/core/tui/src/tui/render.ts +309 -0
  40. package/packages/core/tui/src/tui/theme.ts +145 -0
  41. package/packages/core/tui/src/types/surface.ts +407 -0
  42. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  43. package/plugins/kxm/dist/cli.js +681 -553
  44. package/plugins/kxm/dist/extension.js +18 -18
  45. package/plugins/kxm/dist/mcp-server.js +1 -1
  46. package/plugins/kxm/dist/{vnext-runtime-supervisor.js → runtime-supervisor.js} +553 -553
  47. package/plugins/kxm/dist/runtime.js +608 -608
  48. package/plugins/kxm/dist/server.js +13 -13
  49. package/plugins/kxm/package.json +2 -2
  50. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +2 -2
  51. package/plugins/kxm/skills/kxm-runs/SKILL.md +3 -3
  52. package/plugins/kxm/skills/kxm-session/SKILL.md +1 -1
  53. package/plugins/kxm/skills/kxm-workflow/SKILL.md +1 -1
  54. package/plugins/kxm/src/autocomplete.ts +4 -4
  55. package/plugins/kxm/src/{vnext-bindings.ts → bindings.ts} +48 -48
  56. package/plugins/kxm/src/cli/hub.ts +3 -3
  57. package/plugins/kxm/src/cli/{vnext.ts → project.ts} +106 -106
  58. package/plugins/kxm/src/cli/roles.ts +9 -9
  59. package/plugins/kxm/src/cli/system.ts +2 -2
  60. package/plugins/kxm/src/cli/tasks.ts +8 -8
  61. package/plugins/kxm/src/cli/workflows.ts +8 -8
  62. package/plugins/kxm/src/cli.ts +47 -47
  63. package/plugins/kxm/src/config.ts +69 -14
  64. package/plugins/kxm/src/database.ts +4 -4
  65. package/plugins/kxm/src/{vnext-engine-artifacts.ts → engine-artifacts.ts} +7 -7
  66. package/plugins/kxm/src/{vnext-engine-command.ts → engine-command.ts} +40 -40
  67. package/plugins/kxm/src/{vnext-engine-compile.ts → engine-compile.ts} +64 -64
  68. package/plugins/kxm/src/{vnext-engine-evidence.ts → engine-evidence.ts} +26 -26
  69. package/plugins/kxm/src/{vnext-engine-fold.ts → engine-fold.ts} +99 -99
  70. package/plugins/kxm/src/{vnext-engine-gate-records.ts → engine-gate-records.ts} +105 -105
  71. package/plugins/kxm/src/{vnext-engine-plan.ts → engine-plan.ts} +69 -69
  72. package/plugins/kxm/src/{vnext-engine.ts → engine.ts} +502 -502
  73. package/plugins/kxm/src/gate-hash.ts +10 -0
  74. package/plugins/kxm/src/{vnext-harness.ts → harness.ts} +1 -1
  75. package/plugins/kxm/src/hub.ts +1 -1
  76. package/plugins/kxm/src/init-guide-setup.ts +3 -3
  77. package/plugins/kxm/src/{vnext-init.ts → init.ts} +93 -93
  78. package/plugins/kxm/src/kxm-install-kind.ts +1 -1
  79. package/plugins/kxm/src/kxm-update-config.ts +2 -2
  80. package/plugins/kxm/src/local-snapshot.ts +23 -23
  81. package/plugins/kxm/src/mcp-server.ts +1 -1
  82. package/plugins/kxm/src/{vnext-migrate.ts → migrate.ts} +123 -123
  83. package/plugins/kxm/src/model-inventory.ts +1 -1
  84. package/plugins/kxm/src/{vnext-oneshot-evidence.ts → oneshot-evidence.ts} +2 -2
  85. package/plugins/kxm/src/{vnext-oneshot-process.ts → oneshot-process.ts} +4 -4
  86. package/plugins/kxm/src/{vnext-oneshot-producer.ts → oneshot-producer.ts} +24 -24
  87. package/plugins/kxm/src/{vnext-permission.ts → permission.ts} +86 -86
  88. package/plugins/kxm/src/{vnext-pi-producer.ts → pi-producer.ts} +13 -13
  89. package/plugins/kxm/src/{vnext-config.ts → project-config.ts} +177 -177
  90. package/plugins/kxm/src/{vnext-repair.ts → repair.ts} +159 -159
  91. package/plugins/kxm/src/restricted-yaml.d.mts +3 -3
  92. package/plugins/kxm/src/restricted-yaml.mjs +4 -4
  93. package/plugins/kxm/src/{vnext-runtime-owner.ts → runtime-owner.ts} +35 -35
  94. package/plugins/kxm/src/{vnext-runtime.ts → runtime-service.ts} +136 -136
  95. package/plugins/kxm/src/{vnext-runtime-store.ts → runtime-store.ts} +145 -145
  96. package/plugins/kxm/src/{vnext-runtime-supervisor.ts → runtime-supervisor.ts} +80 -80
  97. package/plugins/kxm/src/runtime.ts +6 -6
  98. package/plugins/kxm/src/session-work.ts +3 -3
  99. package/plugins/kxm/src/studio-layout.ts +6 -6
  100. package/plugins/kxm/src/{vnext-template.ts → template.ts} +33 -33
  101. package/plugins/kxm/src/tui.ts +10 -24
  102. package/schemas/{vnext/README.md → README.md} +3 -3
  103. package/schemas/{vnext/agent.schema.json → agent.schema.json} +1 -1
  104. package/schemas/{vnext/assignment-result.schema.json → assignment-result.schema.json} +1 -1
  105. package/schemas/{vnext/backup-manifest.schema.json → backup-manifest.schema.json} +1 -1
  106. package/schemas/{vnext/candidate.schema.json → candidate.schema.json} +1 -1
  107. package/schemas/{vnext/common.schema.json → common.schema.json} +2 -2
  108. package/schemas/{vnext/context-candidate.schema.json → context-candidate.schema.json} +1 -1
  109. package/schemas/{vnext/context-packet.schema.json → context-packet.schema.json} +1 -1
  110. package/schemas/{vnext/delivery-manifest.schema.json → delivery-manifest.schema.json} +1 -1
  111. package/schemas/{vnext/drive-receipt.schema.json → drive-receipt.schema.json} +2 -2
  112. package/schemas/{vnext/environment.schema.json → environment.schema.json} +1 -1
  113. package/schemas/{vnext/gate-registry.schema.json → gate-registry.schema.json} +1 -1
  114. package/schemas/{vnext/handoff-manifest.schema.json → handoff-manifest.schema.json} +1 -1
  115. package/schemas/{vnext/init-operation.schema.json → init-operation.schema.json} +1 -1
  116. package/schemas/{vnext/local-repository-bindings.schema.json → local-repository-bindings.schema.json} +1 -1
  117. package/schemas/{vnext/memory-record.schema.json → memory-record.schema.json} +1 -1
  118. package/schemas/{vnext/migration-decision.schema.json → migration-decision.schema.json} +1 -1
  119. package/schemas/{vnext/migration-plan.schema.json → migration-plan.schema.json} +1 -1
  120. package/schemas/{vnext/migration-receipt.schema.json → migration-receipt.schema.json} +1 -1
  121. package/schemas/{vnext/model.schema.json → model.schema.json} +1 -1
  122. package/schemas/{vnext/modes.schema.json → modes.schema.json} +1 -1
  123. package/schemas/{vnext/permission-diff.schema.json → permission-diff.schema.json} +1 -1
  124. package/schemas/policy-draft/README.md +2 -2
  125. package/schemas/{vnext/prices.schema.json → prices.schema.json} +1 -1
  126. package/schemas/{vnext/project.schema.json → project.schema.json} +1 -1
  127. package/schemas/{vnext/repository.schema.json → repository.schema.json} +1 -1
  128. package/schemas/{vnext/role.schema.json → role.schema.json} +1 -1
  129. package/schemas/{vnext/run-event.schema.json → run-event.schema.json} +1 -1
  130. package/schemas/{vnext/session-brief.schema.json → session-brief.schema.json} +2 -2
  131. package/schemas/{vnext/sync-event.schema.json → sync-event.schema.json} +1 -1
  132. package/schemas/{vnext/template-provenance.schema.json → template-provenance.schema.json} +1 -1
  133. package/schemas/{vnext/workflow.schema.json → workflow.schema.json} +1 -1
  134. package/scripts/build-package.mjs +59 -0
  135. package/scripts/build-runtime.mjs +3 -2
  136. package/scripts/check-generated.mjs +2 -1
  137. package/scripts/check-versions.mjs +12 -1
  138. package/scripts/docker-install-smoke.mjs +268 -0
  139. package/scripts/emit-codex-artifacts.mjs +1 -1
  140. package/scripts/kxm-bump-version.mjs +53 -3
  141. package/scripts/kxm-hub.mjs +36 -0
  142. package/scripts/kxm-runtime-supervisor.mjs +3 -3
  143. package/scripts/package-surfaces.mjs +39 -0
  144. package/plugins/kxm/src/vnext-gate-hash.ts +0 -10
  145. /package/docs/{vnext → contracts}/effects-and-recovery.md +0 -0
  146. /package/docs/{vnext → contracts}/lifecycles.md +0 -0
  147. /package/docs/{vnext → contracts}/terminology.md +0 -0
  148. /package/examples/{vnext → project}/.kxm/agents/coordinator.yaml +0 -0
  149. /package/examples/{vnext → project}/.kxm/agents/critic-1.yaml +0 -0
  150. /package/examples/{vnext → project}/.kxm/agents/critic-2.yaml +0 -0
  151. /package/examples/{vnext → project}/.kxm/agents/implementer.yaml +0 -0
  152. /package/examples/{vnext → project}/.kxm/agents/planner.yaml +0 -0
  153. /package/examples/{vnext → project}/.kxm/agents/reproducer.yaml +0 -0
  154. /package/examples/{vnext → project}/.kxm/agents/reviewer.yaml +0 -0
  155. /package/examples/{vnext → project}/.kxm/gates.yaml +0 -0
  156. /package/examples/{vnext → project}/.kxm/models/critic-claude.yaml +0 -0
  157. /package/examples/{vnext → project}/.kxm/models/critic-gemini.yaml +0 -0
  158. /package/examples/{vnext → project}/.kxm/models/critic-grok.yaml +0 -0
  159. /package/examples/{vnext → project}/.kxm/models/implementation.yaml +0 -0
  160. /package/examples/{vnext → project}/.kxm/models/primary.yaml +0 -0
  161. /package/examples/{vnext → project}/.kxm/prices.yaml +0 -0
  162. /package/examples/{vnext → project}/.kxm/project/env.yaml +0 -0
  163. /package/examples/{vnext → project}/.kxm/project.yaml +0 -0
  164. /package/examples/{vnext → project}/.kxm/repo/repo.yaml +0 -0
  165. /package/examples/{vnext → project}/.kxm/workflows/default.yaml +0 -0
  166. /package/examples/{vnext → project}/.kxm/workflows/fix.yaml +0 -0
  167. /package/examples/{vnext → project}/.kxm/workflows/improve.yaml +0 -0
  168. /package/examples/{vnext → project}/records/assignment-result-recorded.json +0 -0
  169. /package/examples/{vnext → project}/records/assignment-result.json +0 -0
  170. /package/examples/{vnext → project}/records/context-candidate.json +0 -0
  171. /package/examples/{vnext → project}/records/delivery-manifest.json +0 -0
  172. /package/examples/{vnext → project}/records/effect-uncertainty-resolved-sync.json +0 -0
  173. /package/examples/{vnext → project}/records/effect-uncertainty-resolved.json +0 -0
  174. /package/examples/{vnext → project}/records/run-created.json +0 -0
  175. /package/examples/{vnext → project}/records/sync-event.json +0 -0
  176. /package/examples/{vnext → project}/repositories/api/.kxm/repo/env.yaml +0 -0
  177. /package/examples/{vnext → project}/repositories/api/.kxm/repo/repo.yaml +0 -0
  178. /package/examples/{vnext → project}/repositories/web/.kxm/repo/repo.yaml +0 -0
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ // Fresh-container install smoke: proves the packed kxm tarball installs into a
3
+ // clean Docker container with a brand-new Pi installation, that every runtime
4
+ // dependency lands, and that two configured plan providers complete a live
5
+ // turn. Secrets come from pass-cli and are injected through a mode-600 env
6
+ // file inside a temp directory that is removed afterwards; they never touch
7
+ // argv, the image, or the repository.
8
+ //
9
+ // Usage: just docker-install-smoke
10
+ // Requires: docker daemon, pass-cli session, ~/.pi/agent/models-store.json.
11
+
12
+ import { spawnSync } from "node:child_process";
13
+ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
14
+ import { homedir, tmpdir } from "node:os";
15
+ import { join, resolve } from "node:path";
16
+
17
+ const repoRoot = resolve(import.meta.dirname, "..");
18
+
19
+ const vault = process.env.KXM_SMOKE_VAULT ?? "AI Provider Keys";
20
+ const image = process.env.KXM_SMOKE_IMAGE ?? "node:24-slim";
21
+ const modelsStorePath = process.env.KXM_SMOKE_MODELS_STORE ?? join(homedir(), ".pi/agent/models-store.json");
22
+
23
+ // provider id -> { env, item, field, model }. The zai key is stored in the
24
+ // SshKey public_key slot of its pass item (upstream quirk); the qwen key is an
25
+ // extra Hidden field named "API Key".
26
+ const providers = [
27
+ {
28
+ id: "zai-coding-cn",
29
+ env: "ZAI_CODING_CN_API_KEY",
30
+ item: process.env.KXM_SMOKE_ZAI_ITEM ?? "Z.ai",
31
+ field: process.env.KXM_SMOKE_ZAI_FIELD ?? "public_key",
32
+ model: process.env.KXM_SMOKE_ZAI_MODEL ?? "glm-5.3-flash",
33
+ },
34
+ {
35
+ id: "qwen-token-plan",
36
+ env: "QWEN_TOKEN_PLAN_API_KEY",
37
+ item: process.env.KXM_SMOKE_QWEN_ITEM ?? "Alibaba Cloud Personal Plan",
38
+ field: process.env.KXM_SMOKE_QWEN_FIELD ?? "API Key",
39
+ model: process.env.KXM_SMOKE_QWEN_MODEL ?? "qwen3.8-flash",
40
+ },
41
+ ];
42
+
43
+ function die(message) {
44
+ process.stderr.write(`docker-install-smoke: ${message}\n`);
45
+ process.exit(1);
46
+ }
47
+
48
+ function run(command, args, options = {}) {
49
+ const result = spawnSync(command, args, { encoding: "utf8", ...options });
50
+ if (result.status !== 0) {
51
+ die(`\`${command} ${args.join(" ")}\` failed (${result.status})\n${result.stderr || result.stdout}`);
52
+ }
53
+ return result;
54
+ }
55
+
56
+ function passItem(title) {
57
+ const result = spawnSync(
58
+ "pass-cli",
59
+ ["item", "view", "--vault-name", vault, "--item-title", title, "--output", "json"],
60
+ { encoding: "utf8" },
61
+ );
62
+ if (result.status !== 0) {
63
+ die(`pass-cli could not read item ${JSON.stringify(title)} in vault ${JSON.stringify(vault)}: ${result.stderr}`);
64
+ }
65
+ return JSON.parse(result.stdout)?.item?.content;
66
+ }
67
+
68
+ // Field values hide in either extra_fields (by name) or the SshKey section.
69
+ function extractField(content, field) {
70
+ const extra = content.extra_fields?.find((candidate) => candidate.name === field);
71
+ if (extra) {
72
+ const values = extra.content ?? {};
73
+ return values.Hidden ?? values.Text;
74
+ }
75
+ return content.content?.SshKey?.[field];
76
+ }
77
+
78
+ function main() {
79
+ for (const tool of ["docker", "pass-cli", "npm"]) {
80
+ if (spawnSync(tool, ["--version"], { encoding: "utf8" }).status !== 0) {
81
+ die(`${tool} is required but not on PATH`);
82
+ }
83
+ }
84
+ const daemon = spawnSync("docker", ["info", "--format", "ok"], { encoding: "utf8" });
85
+ if (daemon.status !== 0) {
86
+ die("docker daemon is not reachable; start Docker and retry");
87
+ }
88
+
89
+ const scratch = mkdtempSync(join(tmpdir(), "kxm-install-smoke-"));
90
+ try {
91
+ // 1. Pack the working tree. The committed dist must be current; the
92
+ // commit gate (`npm run verify` -> check:generated) enforces that.
93
+ process.stdout.write("==> npm pack\n");
94
+ const packed = run("npm", ["pack", "--pack-destination", scratch, "--json"], { cwd: repoRoot });
95
+ const filename = JSON.parse(packed.stdout)[0]?.filename;
96
+ if (!filename) die("npm pack produced no tarball");
97
+ const tarball = join(scratch, filename);
98
+
99
+ // 2. Provider models from the local pi catalog (no secrets in this file).
100
+ const store = JSON.parse(readFileSync(modelsStorePath, "utf8"));
101
+ const configured = {};
102
+ const envLines = [];
103
+ for (const provider of providers) {
104
+ const entry = store[provider.id];
105
+ if (!entry) {
106
+ die(`provider ${provider.id} is missing from ${modelsStorePath}`);
107
+ }
108
+ const models = entry.models.map((model) => {
109
+ const copy = { ...model };
110
+ delete copy.checkedAt;
111
+ delete copy.lastModified;
112
+ delete copy.etag;
113
+ return copy;
114
+ });
115
+ if (models.length === 0) die(`provider ${provider.id} has no models in ${modelsStorePath}`);
116
+ configured[provider.id] = {
117
+ baseUrl: models[0].baseUrl,
118
+ api: models[0].api ?? "openai-completions",
119
+ apiKey: `$${provider.env}`,
120
+ models,
121
+ };
122
+ const key = extractField(passItem(provider.item), provider.field);
123
+ if (!key) {
124
+ die(`no value found for ${provider.item} / ${provider.field} in vault ${vault}`);
125
+ }
126
+ process.stdout.write(`==> key for ${provider.id}: item=${JSON.stringify(provider.item)} field=${provider.field} len=${key.length}\n`);
127
+ envLines.push(`${provider.env}=${key}`);
128
+ }
129
+ const modelsJson = join(scratch, "models.json");
130
+ writeFileSync(modelsJson, `${JSON.stringify({ providers: configured }, null, 2)}\n`);
131
+
132
+ // 3. Keys go into a mode-600 env file only; docker --env-file injects them
133
+ // into the container environment without ever appearing in argv.
134
+ const envFile = join(scratch, "providers.env");
135
+ writeFileSync(envFile, `${envLines.join("\n")}\n`, { mode: 0o600 });
136
+ chmodSync(envFile, 0o600);
137
+
138
+ // 4. The in-container flow: fresh pi, both providers live, kxm operator
139
+ // install with the documented --omit=peer path, pi package install,
140
+ // then the README first-run flow.
141
+ const smokeModels = providers.map((provider) => ({
142
+ id: provider.id,
143
+ env: provider.env,
144
+ model: provider.model,
145
+ token: `${provider.id.toUpperCase().replaceAll(/[^A-Z]/g, "")}_SMOKE_OK`,
146
+ }));
147
+ const container = join(scratch, "container.sh");
148
+ writeFileSync(container, containerScript(smokeModels, filename));
149
+
150
+ process.stdout.write(`==> docker run ${image}\n`);
151
+ const result = spawnSync("docker", [
152
+ "run", "--rm",
153
+ "-v", `${scratch}:/mnt:ro`,
154
+ "--env-file", envFile,
155
+ image, "bash", "/mnt/container.sh",
156
+ ], { stdio: "inherit" });
157
+ if (result.status !== 0) {
158
+ die(`container smoke failed with ${result.status}`);
159
+ }
160
+ process.stdout.write("==> install smoke passed\n");
161
+ } finally {
162
+ rmSync(scratch, { recursive: true, force: true });
163
+ }
164
+ }
165
+
166
+ function containerScript(smokeModels, filename) {
167
+ const [first, second] = smokeModels;
168
+ return `#!/bin/bash
169
+ # Generated by scripts/docker-install-smoke.mjs. Runs inside a clean container.
170
+ set -uo pipefail
171
+ fail() { echo "FAIL: $*" >&2; exit 1; }
172
+
173
+ echo "== toolchain versions =="
174
+ node --version
175
+ npm --version
176
+ apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq git >/dev/null 2>&1
177
+ git --version
178
+
179
+ echo
180
+ echo "== [1/7] fresh pi installation (npm global, --ignore-scripts) =="
181
+ npm install -g --ignore-scripts @earendil-works/pi-coding-agent --no-audit --no-fund >/tmp/pi-install.log 2>&1 \\
182
+ || { tail -20 /tmp/pi-install.log; fail "pi global install failed"; }
183
+ pi --version || fail "pi not on PATH after global install"
184
+
185
+ echo
186
+ echo "== [2/7] custom providers load with env-var auth =="
187
+ mkdir -p ~/.pi/agent
188
+ cp /mnt/models.json ~/.pi/agent/models.json
189
+ ${smokeModels.map((provider) => `env | grep -q '^${provider.env}=.' || fail "${provider.env} env missing"`).join("\n")}
190
+ LM=$(pi --list-models 2>&1) || fail "pi --list-models failed: $LM"
191
+ ${smokeModels.map((provider) => `echo "$LM" | grep -qi "${provider.id}" || fail "${provider.id} models not available: $LM"`).join("\n")}
192
+ ${smokeModels.map((provider) => `echo " ${provider.id}: $(echo "$LM" | grep -ci '${provider.id}') models listed"`).join("\n")}
193
+
194
+ echo
195
+ echo "== [3/7] live completion: ${first.id} / ${first.model} =="
196
+ Z=$(pi --provider ${first.id} --model ${first.model} -p "Reply with exactly this token and nothing else: ${first.token}") \\
197
+ || fail "${first.id} completion failed"
198
+ echo " reply: $Z"
199
+ echo "$Z" | grep -q "${first.token}" || fail "${first.id} reply missing expected token"
200
+
201
+ echo
202
+ echo "== [4/7] live completion: ${second.id} / ${second.model} =="
203
+ Q=$(pi --provider ${second.id} --model ${second.model} -p "Reply with exactly this token and nothing else: ${second.token}") \\
204
+ || fail "${second.id} completion failed"
205
+ echo " reply: $Q"
206
+ echo "$Q" | grep -q "${second.token}" || fail "${second.id} reply missing expected token"
207
+
208
+ echo
209
+ echo "== [5/7] kxm operator CLI: documented global --omit=peer tarball install =="
210
+ GROOT_BEFORE=$(ls "$(npm root -g)")
211
+ npm install --global --omit=peer --no-audit --no-fund /mnt/${filename} >/tmp/kxm-install.log 2>&1 \\
212
+ || { tail -20 /tmp/kxm-install.log; fail "kxm global install failed"; }
213
+ kxm hub help 2>&1 | grep -q "Usage: kxm hub" || fail "kxm hub help did not run (runtime deps missing?)"
214
+ echo " kxm bin runs: OK"
215
+ GROOT=$(npm root -g)
216
+ for d in @anthropic-ai/claude-agent-sdk @earendil-works/pi-tui @modelcontextprotocol/sdk yaml; do
217
+ if [ -d "$GROOT/@kontextmind/kxm/node_modules/$d" ] || [ -d "$GROOT/$d" ]; then
218
+ echo " dependency installed: $d"
219
+ else
220
+ fail "dependency NOT installed: $d"
221
+ fi
222
+ done
223
+ if [ -d "$GROOT/typebox" ] && ! echo "$GROOT_BEFORE" | grep -q "^typebox$"; then
224
+ fail "peer typebox was installed despite --omit=peer"
225
+ fi
226
+ echo " peers correctly omitted (--omit=peer); CLI still runs"
227
+
228
+ echo
229
+ echo "== [6/7] pi package surface (extension + skills) =="
230
+ mkdir -p /opt/kxm-pkg
231
+ tar -xzf /mnt/${filename} -C /opt/kxm-pkg --strip-components=1
232
+ cd /opt/kxm-pkg
233
+ npm install --omit=dev --no-audit --no-fund >/tmp/pkg-install.log 2>&1 \\
234
+ || { tail -20 /tmp/pkg-install.log; fail "package npm install failed (pi git-install step)"; }
235
+ pi install /opt/kxm-pkg >/tmp/pi-install-pkg.log 2>&1 || { tail -20 /tmp/pi-install-pkg.log; fail "pi install of kxm package failed"; }
236
+ pi list 2>&1 | grep -qi "kxm" || fail "pi list does not show kxm"
237
+ echo " pi list shows kxm package: OK"
238
+
239
+ echo
240
+ echo "== [7/7] first-run flow: kxm init + hub + bind + session brief =="
241
+ mkdir -p /testproj && cd /testproj && git init -q .
242
+ kxm init --json --name docker-smoke --project-id prj_01JDOCKERTESTSMOKE000000000 || fail "kxm init failed"
243
+ export KXM_AUTH_TOKEN="docker-admin-token"
244
+ export KXM_PROJECT_TOKENS='{"docker-smoke":"proj-token"}'
245
+ nohup kxm hub start >/tmp/hub.log 2>&1 &
246
+ for i in $(seq 1 30); do grep -q "listening at" /tmp/hub.log 2>/dev/null && break; sleep 1; done
247
+ grep "listening at" /tmp/hub.log || { cat /tmp/hub.log; fail "hub did not start"; }
248
+ kxm hub bind http://127.0.0.1:7331 || fail "hub bind failed"
249
+ kxm session brief >/dev/null || fail "session brief failed"
250
+ echo " first-run flow: OK"
251
+
252
+ echo
253
+ echo "== final: pi print-mode run with kxm extension loaded =="
254
+ OUT=$(pi --provider ${first.id} --model ${first.model} -p "Reply with exactly: FINAL_OK" 2>/tmp/pi-ext.err) \\
255
+ || { cat /tmp/pi-ext.err; fail "final pi run failed"; }
256
+ if grep -iE "failed to load|extension error|cannot find" /tmp/pi-ext.err >/dev/null; then
257
+ cat /tmp/pi-ext.err; fail "kxm extension load errors in pi"
258
+ fi
259
+ echo " no extension load errors"
260
+ echo " final reply: $OUT"
261
+ echo "$OUT" | grep -q "FINAL_OK" || fail "final reply missing expected token"
262
+
263
+ echo
264
+ echo "ALL CHECKS PASSED"
265
+ `;
266
+ }
267
+
268
+ main();
@@ -45,7 +45,7 @@ The \`kxm\` CLI is the unified agent surface for peer collaboration and workflow
45
45
  | \`kxm workflow checkpoint [runId] [stageId] [status] [summary]\` | Record stage result with verified evidence | \`--run-id\`, \`--stage-id\`, \`--status <passed\\|warning\\|failed>\`, \`--summary\`, \`--evidence <json>\`, \`--evidence-refs <json>\` |
46
46
  | \`kxm workflow record [runId] [category] [area] [summary]\` | Record plans, decisions, contradictions, errors, lessons | \`--run-id\`, \`--category <plan\\|decision\\|contradiction\\|error\\|lesson>\`, \`--area\`, \`--severity <info\\|warning\\|error>\`, \`--details\`, \`--evidence <items...>\` |
47
47
  | \`kxm workflow wait [runId] [stageId] [signalKey] [summary]\` | Pause stage until an external signed signal arrives | \`--run-id\`, \`--stage-id\`, \`--signal-key\`, \`--summary\`, \`--evidence <json>\`, \`--evidence-refs <json>\`, \`--timeout-ms\` |
48
- | \`kxm workflow signal <runId> <signalKey> <status> <summary>\` | Resume or unblock a waiting stage or vNext run | \`[evidence...]\`, \`--delivery-id\` |
48
+ | \`kxm workflow signal <runId> <signalKey> <status> <summary>\` | Resume or unblock a waiting stage or KXM run | \`[evidence...]\`, \`--delivery-id\` |
49
49
  | \`kxm workflow list\` | List local workflow runs | \`--json\` |
50
50
  | \`kxm workflow get <runId>\` | Get stages and journal for a run | \`--json\` |
51
51
 
@@ -9,14 +9,22 @@
9
9
  * node scripts/kxm-bump-version.mjs minor # bump minor
10
10
  * node scripts/kxm-bump-version.mjs major # bump major
11
11
  *
12
- * Surfaces updated (must stay in sync with scripts/check-versions.mjs):
12
+ * Surfaces updated (must stay in sync with scripts/check-versions.mjs — both
13
+ * take their package list from scripts/package-surfaces.mjs):
13
14
  * 1. package.json .version
14
15
  * 2. package-lock.json .version + .packages[""].version
16
+ * + .packages[<workspace path>].version
15
17
  * 3. plugins/kxm/package.json .version
16
18
  * 4. plugins/kxm/.claude-plugin/plugin.json .version
17
19
  * 5. .claude-plugin/marketplace.json .plugins[name=kxm].version
18
20
  * 6. plugins/kxm/src/mcp-server.ts const VERSION = "..."
19
21
  * 7. plugins/kxm/dist/mcp-server.js const VERSION = "..." (rebuilt)
22
+ * 8. each workspace manifest scanned under packages/ —
23
+ * packages/<name>/package.json and packages/<tier>/<name>/package.json
24
+ *
25
+ * The lockfile is patched by key, never by searching for the old version string:
26
+ * a third-party dependency that happens to carry the same version as the
27
+ * product must not be rewritten into a phantom artifact.
20
28
  *
21
29
  * After running this script, run `npm run build` to rebuild dist bundles.
22
30
  */
@@ -24,6 +32,7 @@
24
32
  import { readFileSync, writeFileSync } from "node:fs";
25
33
  import { resolve } from "node:path";
26
34
  import { fileURLToPath } from "node:url";
35
+ import { workspaceManifestPaths } from "./package-surfaces.mjs";
27
36
 
28
37
  const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
29
38
 
@@ -79,6 +88,43 @@ function patchSourceVersion(filePath, newVersion) {
79
88
  writeFileSync(filePath, patched, "utf8");
80
89
  }
81
90
 
91
+ /**
92
+ * Patch the lockfile structurally: the root entry and each workspace package
93
+ * entry, by key. Everything else — including an unrelated dependency whose
94
+ * version string equals ours — is left byte-for-byte alone.
95
+ */
96
+ function patchLockVersions(root, newVersion) {
97
+ const file = resolve(root, "package-lock.json");
98
+ const raw = readFileSync(file, "utf8");
99
+ let data;
100
+ try {
101
+ data = JSON.parse(raw);
102
+ } catch (error) {
103
+ throw new Error(`package-lock.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
104
+ }
105
+ const touched = [];
106
+ if (typeof data.version === "string") {
107
+ data.version = newVersion;
108
+ touched.push("version");
109
+ }
110
+ if (data.packages?.[""]) {
111
+ data.packages[""].version = newVersion;
112
+ touched.push('packages[""]');
113
+ }
114
+ for (const rel of workspaceManifestPaths(root)) {
115
+ const entry = data.packages?.[rel];
116
+ if (!entry) {
117
+ throw new Error(`package-lock.json has no entry for workspace package ${rel}; run npm install to resync the lock`);
118
+ }
119
+ entry.version = newVersion;
120
+ touched.push(`packages[${rel}]`);
121
+ }
122
+ if (touched.length === 0) throw new Error("no version fields found to patch in package-lock.json");
123
+ const trailingNewline = raw.endsWith("\n") ? "\n" : "";
124
+ writeFileSync(file, `${JSON.stringify(data, null, 2)}${trailingNewline}`, "utf8");
125
+ return touched;
126
+ }
127
+
82
128
  export function applyVersionBump(root, newVersion) {
83
129
  if (!parseSemver(newVersion)) {
84
130
  throw new Error(`invalid semver: ${newVersion}`);
@@ -88,18 +134,22 @@ export function applyVersionBump(root, newVersion) {
88
134
  const pkgPath = resolve(root, "package.json");
89
135
  const oldVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version;
90
136
 
91
- // JSON surfaces (targeted patch preserves formatting)
137
+ // JSON manifests (targeted patch preserves formatting). Workspace packages are
138
+ // scanned, not hard-coded: a new package must not be releasable-then-forgotten.
92
139
  const jsonFiles = [
93
140
  "package.json",
94
- "package-lock.json",
95
141
  "plugins/kxm/package.json",
96
142
  "plugins/kxm/.claude-plugin/plugin.json",
97
143
  ".claude-plugin/marketplace.json",
144
+ ...workspaceManifestPaths(root).map((rel) => `${rel}/package.json`),
98
145
  ];
99
146
  for (const rel of jsonFiles) {
100
147
  patchJsonVersion(resolve(root, rel), oldVersion, newVersion);
101
148
  }
102
149
 
150
+ // Lockfile by key, after the manifests so a missing workspace entry fails loudly.
151
+ patchLockVersions(root, newVersion);
152
+
103
153
  // TypeScript source
104
154
  patchSourceVersion(resolve(root, "plugins/kxm/src/mcp-server.ts"), newVersion);
105
155
 
@@ -39,6 +39,29 @@ function processExists(pid) {
39
39
  }
40
40
  }
41
41
 
42
+ /**
43
+ * Live hub claim, if any. Read-only: returns the pid of a well-formed
44
+ * `hub.pid` claim whose wrapper is still alive, otherwise null.
45
+ *
46
+ * Claims that are missing, malformed, dead, or stale return null so the
47
+ * atomic `claimPidFile()` below keeps sole authority over those cases — it is
48
+ * the real single-instance guard and fails closed on them. This exists only so
49
+ * a start that is certainly going to be refused can refuse *before* it
50
+ * persists credentials.
51
+ */
52
+ function liveHubClaimPid() {
53
+ let existing;
54
+ try {
55
+ existing = JSON.parse(readFileSync(pidPath, "utf8"));
56
+ } catch {
57
+ return null;
58
+ }
59
+ if (existing?.version === 1 && existing.role === "hub" && Number.isInteger(existing.pid) && processExists(existing.pid)) {
60
+ return existing.pid;
61
+ }
62
+ return null;
63
+ }
64
+
42
65
  function parseHubEnvFile() {
43
66
  const file = join(resolveUserStateRoot(), "hub-env.json");
44
67
  let raw;
@@ -122,6 +145,19 @@ function resolveCredentials() {
122
145
  return { authToken, authTokenSource, projectTokens, projectTokensSource, file };
123
146
  }
124
147
 
148
+ // Pre-flight: refuse a start that a live hub already owns before
149
+ // resolveCredentials() can generate and persist an admin token that the
150
+ // running hub never issued. `kxm hub start` used to mint the credential at
151
+ // module load and only then report "already managed by PID <n>", leaving a
152
+ // hub-env.json whose token no process trusts.
153
+ try {
154
+ const livePid = liveHubClaimPid();
155
+ if (livePid) {
156
+ process.stderr.write(`kxm hub: KXM hub is already managed by PID ${livePid}. Run kxm hub stop before starting another hub.\n`);
157
+ process.exit(1);
158
+ }
159
+ } catch { /* the atomic claim below still guards startup */ }
160
+
125
161
  let credentials;
126
162
  try {
127
163
  credentials = resolveCredentials();
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // KXM vNext Runtime supervisor entry point. Detached child of `kxm runtime start`.
3
- import { startVnextRuntimeSupervisor } from "../plugins/kxm/dist/vnext-runtime-supervisor.js";
2
+ // KXM Runtime supervisor entry point. Detached child of `kxm runtime start`.
3
+ import { startKxmRuntimeSupervisor } from "../plugins/kxm/dist/runtime-supervisor.js";
4
4
 
5
- const supervisor = await startVnextRuntimeSupervisor({});
5
+ const supervisor = await startKxmRuntimeSupervisor({});
6
6
  process.stdout.write(`kxm-runtime-supervisor ready runtimeId=${supervisor.runtimeId} port=${supervisor.port}\n`);
7
7
 
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The single source of truth for "which directories are KXM packages".
3
+ *
4
+ * `scripts/check-versions.mjs` (the gate) and `scripts/kxm-bump-version.mjs`
5
+ * (the release writer) must agree exactly, or a release bumps some surfaces and
6
+ * not others: the 0.7.41 release job failed because a workspace manifest existed
7
+ * for the checker but not for the bumper. Both now import this scan.
8
+ */
9
+
10
+ import { existsSync, readdirSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+
13
+ /**
14
+ * Every package directory under `packages/`, one or two levels deep
15
+ * (`packages/<name>`, `packages/<tier>/<name>`), as POSIX-style paths relative
16
+ * to `root` — the same shape `package-lock.json` uses for its `packages` keys.
17
+ *
18
+ * A directory that itself carries a `package.json` is a package and is not
19
+ * descended into further.
20
+ */
21
+ export function workspaceManifestPaths(root) {
22
+ const found = [];
23
+ const packagesDir = resolve(root, "packages");
24
+ const scan = (absoluteDir, relativeDir, depth) => {
25
+ if (depth > 2 || !existsSync(absoluteDir)) return;
26
+ for (const entry of readdirSync(absoluteDir, { withFileTypes: true })) {
27
+ if (!entry.isDirectory() || entry.name === "node_modules") continue;
28
+ const abs = join(absoluteDir, entry.name);
29
+ const rel = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
30
+ if (existsSync(join(abs, "package.json"))) {
31
+ found.push(rel);
32
+ continue;
33
+ }
34
+ scan(abs, rel, depth + 1);
35
+ }
36
+ };
37
+ scan(packagesDir, "packages", 1);
38
+ return found.sort();
39
+ }
@@ -1,10 +0,0 @@
1
- import { vnextCanonicalJson, type JsonValue } from "./vnext-config.ts";
2
- import { vnextSha256 } from "./vnext-engine-plan.ts";
3
-
4
- export function gateDefinitionHash(id: string, definition: JsonValue): string {
5
- return vnextSha256(vnextCanonicalJson({ id, ...(definition as object) } as JsonValue));
6
- }
7
-
8
- export function gateRegistryHash(registryValue: JsonValue): string {
9
- return vnextSha256(vnextCanonicalJson(registryValue));
10
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes