@kontextmind/kxm 0.6.0 → 0.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +7 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +5 -0
  14. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  15. package/docs/agent-skills.md +135 -0
  16. package/docs/architecture.md +1 -1
  17. package/docs/assignment-runner.md +21 -8
  18. package/docs/browser-automation.md +116 -0
  19. package/docs/configuration.md +11 -2
  20. package/docs/getting-started.md +21 -0
  21. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  22. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  23. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  24. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  25. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  26. package/docs/kb/how-to-take-over-session.md +32 -0
  27. package/docs/kb/why-authentication-disappeared.md +32 -0
  28. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  29. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  30. package/docs/kxm-handbook.md +3 -3
  31. package/docs/operations.md +24 -0
  32. package/docs/operator-pi-packages.md +63 -0
  33. package/docs/prompts/browser-annotate-feedback.md +41 -0
  34. package/docs/prompts/browser-diagnose-recover.md +38 -0
  35. package/docs/prompts/browser-explore.md +42 -0
  36. package/docs/prompts/browser-repro-fix.md +48 -0
  37. package/docs/prompts/browser-start.md +41 -0
  38. package/docs/prompts/browser-takeover.md +50 -0
  39. package/docs/skills/repo-work-delivery.md +107 -0
  40. package/docs/skills.md +2 -0
  41. package/docs/test-matrix.md +4 -3
  42. package/docs/troubleshooting.md +41 -1
  43. package/docs/vnext/validation.md +9 -0
  44. package/docs/webhook-workflows.md +2 -2
  45. package/examples/README.md +1 -1
  46. package/package.json +16 -17
  47. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  48. package/plugins/kxm/README.md +1 -1
  49. package/plugins/kxm/dist/cli.js +41620 -35578
  50. package/plugins/kxm/dist/core.js +271 -34
  51. package/plugins/kxm/dist/extension.js +7759 -86
  52. package/plugins/kxm/dist/mcp-server.js +75 -21
  53. package/plugins/kxm/dist/runtime.js +8218 -2328
  54. package/plugins/kxm/dist/server.js +3125 -2260
  55. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
  56. package/plugins/kxm/package.json +1 -1
  57. package/plugins/kxm/skills/SUITE.md +5 -0
  58. package/plugins/kxm/skills/hints.json +103 -0
  59. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  60. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  61. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  62. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  63. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  64. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  65. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  66. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  67. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  68. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  69. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  70. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  71. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  72. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  73. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  74. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  75. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  76. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  77. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  78. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  79. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  80. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  81. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  82. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  83. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  84. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  85. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  86. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  87. package/plugins/kxm/src/autocomplete.ts +9 -3
  88. package/plugins/kxm/src/browser.ts +603 -0
  89. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  90. package/plugins/kxm/src/cli/hub.ts +614 -0
  91. package/plugins/kxm/src/cli/roles.ts +615 -0
  92. package/plugins/kxm/src/cli/system.ts +906 -0
  93. package/plugins/kxm/src/cli/tasks.ts +364 -0
  94. package/plugins/kxm/src/cli/types.ts +270 -0
  95. package/plugins/kxm/src/cli/vnext.ts +698 -0
  96. package/plugins/kxm/src/cli/workflows.ts +699 -0
  97. package/plugins/kxm/src/cli.ts +362 -2849
  98. package/plugins/kxm/src/commands.ts +150 -8
  99. package/plugins/kxm/src/completion-install.ts +223 -0
  100. package/plugins/kxm/src/config.ts +7 -4
  101. package/plugins/kxm/src/context-packet.ts +172 -0
  102. package/plugins/kxm/src/database.ts +1 -1
  103. package/plugins/kxm/src/extension.ts +36 -1
  104. package/plugins/kxm/src/external-effects.ts +357 -8
  105. package/plugins/kxm/src/hub-env.ts +193 -0
  106. package/plugins/kxm/src/hub.ts +2 -4
  107. package/plugins/kxm/src/improve.ts +72 -0
  108. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  109. package/plugins/kxm/src/local-snapshot.ts +1 -1
  110. package/plugins/kxm/src/mcp-server.ts +1 -1
  111. package/plugins/kxm/src/model-inventory.ts +127 -0
  112. package/plugins/kxm/src/modes.ts +348 -0
  113. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  114. package/plugins/kxm/src/policy-draft.mjs +565 -0
  115. package/plugins/kxm/src/price-calc.ts +17 -18
  116. package/plugins/kxm/src/prices.ts +32 -16
  117. package/plugins/kxm/src/producers.ts +71 -0
  118. package/plugins/kxm/src/protocol.ts +111 -0
  119. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  120. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  121. package/plugins/kxm/src/role.ts +710 -0
  122. package/plugins/kxm/src/routing.ts +99 -1
  123. package/plugins/kxm/src/runtime.ts +4 -0
  124. package/plugins/kxm/src/safety-integrity.ts +76 -0
  125. package/plugins/kxm/src/session-work.ts +9 -2
  126. package/plugins/kxm/src/sqlite.ts +76 -0
  127. package/plugins/kxm/src/ssh-remote.ts +560 -0
  128. package/plugins/kxm/src/store.ts +1 -1
  129. package/plugins/kxm/src/studio-layout.ts +660 -17
  130. package/plugins/kxm/src/subagent-control.ts +312 -0
  131. package/plugins/kxm/src/suggest.ts +7 -13
  132. package/plugins/kxm/src/telemetry.ts +82 -0
  133. package/plugins/kxm/src/tui.ts +140 -0
  134. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  135. package/plugins/kxm/src/vnext-config.ts +53 -111
  136. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  137. package/plugins/kxm/src/vnext-engine.ts +214 -62
  138. package/plugins/kxm/src/vnext-harness.ts +336 -84
  139. package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
  140. package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
  141. package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
  142. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  143. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  144. package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
  145. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  146. package/plugins/kxm/src/workflow-manager.ts +392 -0
  147. package/plugins/kxm/src/workflow-tui.ts +255 -0
  148. package/plugins/kxm/src/workflow.ts +144 -0
  149. package/schemas/policy-draft/README.md +17 -0
  150. package/schemas/policy-draft/model.v2.schema.json +140 -0
  151. package/schemas/policy-draft/role.v2.schema.json +91 -0
  152. package/schemas/vnext/modes.schema.json +56 -0
  153. package/schemas/vnext/role.schema.json +76 -0
  154. package/schemas/vnext/run-event.schema.json +1 -0
  155. package/scripts/assignment-run.d.mts +1 -1
  156. package/scripts/assignment-run.mjs +44 -35
  157. package/scripts/check-generated.mjs +33 -9
  158. package/scripts/emit-codex-artifacts.mjs +255 -11
  159. package/scripts/harness-run.d.mts +12 -4
  160. package/scripts/harness-run.mjs +65 -17
  161. package/scripts/kxm-bump-version.mjs +146 -0
  162. package/scripts/kxm-hub.mjs +150 -2
  163. package/scripts/kxm-publish-npm.mjs +3 -1
  164. package/scripts/kxm-release-github.mjs +3 -1
  165. package/scripts/kxm.mjs +0 -0
  166. package/scripts/native-critic.d.mts +5 -0
  167. package/scripts/native-critic.mjs +60 -0
  168. package/.kxm/config/README.md +0 -5
  169. package/.kxm/config/agents.json +0 -43
  170. package/.kxm/config/env.example +0 -56
  171. package/.kxm/config/update.example.yaml +0 -9
  172. package/.kxm/config/workflows/fix.json +0 -160
  173. package/.kxm/config/workflows/jira-development.json +0 -116
  174. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  175. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -1,11 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
3
+ import {
4
+ cpSync,
5
+ existsSync,
6
+ lstatSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ realpathSync,
11
+ rmSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { join, resolve, sep } from "node:path";
5
15
  import { fileURLToPath } from "node:url";
6
16
 
7
17
  const MARKER_START = "<!-- kxm:codex:commands:start -->";
8
18
  const MARKER_END = "<!-- kxm:codex:commands:end -->";
19
+ const SKILL_NAME = /^[a-z][a-z0-9-]*[a-z0-9]$/u;
20
+ const COMMAND_NAME = /^[a-z][a-z0-9-]*$/u;
21
+ const MANIFEST_REL = "plugins/kxm/skill-suite.json";
9
22
 
10
23
  export const CODEX_COMMANDS_BLOCK = `${MARKER_START}
11
24
  ## KXM agent commands
@@ -44,24 +57,243 @@ The \`kxm\` CLI is the unified agent surface for peer collaboration and workflow
44
57
  | \`kxm context recall <project>\` | Search durable context metadata | \`--query\`, \`--kinds\`, \`--limit\` |
45
58
  | \`kxm context state <project> <key>\` | Query authoritative temporal state | \`--as-of <timestamp>\` |
46
59
  | \`kxm context episode <project>\` | Query workflow learning episodes | \`--run\` |
47
- | \`kxm context promote <project> <key>\` | Propose temporal state change | \`--summary\`, \`--authority\`, \`--confidence\`, \`--evidence\` |
60
+ | \`kxm context promote <project> <proposalId>\` | Promote an approved state proposal (control plane) | required \`--evidence <refs>\` |
48
61
  ${MARKER_END}`;
49
62
 
63
+ function refuse(message) {
64
+ throw new Error(message);
65
+ }
66
+
67
+ function isPlainObject(value) {
68
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
69
+ }
70
+
71
+ function posixContained(value, label) {
72
+ if (typeof value !== "string" || !value || value.includes("\\") || value.includes("\0")) {
73
+ refuse(`${label} must be a contained relative path`);
74
+ }
75
+ const normalized = value.startsWith("./") ? value.slice(2) : value;
76
+ if (!normalized || normalized.startsWith("/") || normalized.split("/").some((part) => !part || part === "." || part === "..")) {
77
+ refuse(`${label} must be a contained relative path`);
78
+ }
79
+ return value;
80
+ }
81
+
82
+ function containedRoot(root) {
83
+ return existsSync(root) ? realpathSync(root) : resolve(root);
84
+ }
85
+
86
+ function assertPathStaysInRoot(resolvedRoot, current, relativePath) {
87
+ const real = realpathSync(current);
88
+ const prefix = resolvedRoot.endsWith(sep) ? resolvedRoot : `${resolvedRoot}${sep}`;
89
+ if (real !== resolvedRoot && !real.startsWith(prefix)) {
90
+ refuse(`path escapes repository: ${relativePath}`);
91
+ }
92
+ return real;
93
+ }
94
+
95
+ function assertExistingAncestorsAreRegularContained(root, relativePath, { required = false } = {}) {
96
+ const resolvedRoot = containedRoot(root);
97
+ const parts = posixContained(relativePath, "path").split("/");
98
+ let current = resolvedRoot;
99
+ for (let i = 0; i < parts.length; i++) {
100
+ current = join(current, parts[i]);
101
+ const rel = parts.slice(0, i + 1).join("/");
102
+ let info;
103
+ try {
104
+ info = lstatSync(current);
105
+ } catch {
106
+ if (required) refuse(`path is missing: ${rel}`);
107
+ return;
108
+ }
109
+ if (info.isSymbolicLink()) refuse(`symlinks are not allowed: ${rel}`);
110
+ if (!info.isDirectory()) {
111
+ refuse(`path must be a regular contained directory: ${rel}`);
112
+ }
113
+ assertPathStaysInRoot(resolvedRoot, current, rel);
114
+ }
115
+ }
116
+
117
+ function assertRegularContainedPath(root, relativePath, { directory = false } = {}) {
118
+ const resolvedRoot = containedRoot(root);
119
+ const parts = posixContained(relativePath, "path").split("/");
120
+ let current = resolvedRoot;
121
+ for (let i = 0; i < parts.length; i++) {
122
+ current = join(current, parts[i]);
123
+ let info;
124
+ try {
125
+ info = lstatSync(current);
126
+ } catch {
127
+ refuse(`path is missing: ${relativePath}`);
128
+ }
129
+ if (info.isSymbolicLink()) refuse(`symlinks are not allowed: ${relativePath}`);
130
+ const last = i === parts.length - 1;
131
+ if (last ? (directory ? !info.isDirectory() : !info.isFile()) : !info.isDirectory()) {
132
+ refuse(`path must be a regular contained ${directory ? "directory" : "file"}: ${relativePath}`);
133
+ }
134
+ }
135
+ assertPathStaysInRoot(resolvedRoot, current, relativePath);
136
+ return current;
137
+ }
138
+
139
+ function validateSkillSuiteManifest(manifest) {
140
+ if (!isPlainObject(manifest)) refuse("skill suite manifest must be an object");
141
+ for (const key of ["id", "version", "name", "description"]) {
142
+ if (typeof manifest[key] !== "string" || !manifest[key].trim()) {
143
+ refuse(`skill suite manifest missing ${key}`);
144
+ }
145
+ }
146
+ if (!Array.isArray(manifest.skills) || manifest.skills.length === 0) {
147
+ refuse("skill suite manifest skills must be a nonempty array");
148
+ }
149
+ const names = new Set();
150
+ const commands = new Set();
151
+ for (const skill of manifest.skills) {
152
+ if (!isPlainObject(skill)) refuse("skill suite entry must be an object");
153
+ const extra = Object.keys(skill).filter((key) => !["name", "path", "ownedCommands", "intent"].includes(key));
154
+ if (extra.length > 0) refuse(`skill suite entry has unknown field(s): ${extra.join(", ")}`);
155
+ if (typeof skill.name !== "string" || !SKILL_NAME.test(skill.name)) {
156
+ refuse(`invalid skill name: ${skill.name ?? ""}`);
157
+ }
158
+ if (names.has(skill.name)) refuse(`duplicate skill name: ${skill.name}`);
159
+ names.add(skill.name);
160
+ posixContained(skill.path, `skill path for ${skill.name}`);
161
+ if (!skill.path.startsWith("./skills/") || skill.path !== `./skills/${skill.name}`) {
162
+ refuse(`skill path must be ./skills/${skill.name}`);
163
+ }
164
+ if (typeof skill.intent !== "string" || skill.intent.length < 10 || skill.intent.length > 200) {
165
+ refuse(`skill ${skill.name} intent must be 10-200 characters`);
166
+ }
167
+ if (!Array.isArray(skill.ownedCommands) || skill.ownedCommands.some((cmd) => typeof cmd !== "string" || !COMMAND_NAME.test(cmd))) {
168
+ refuse(`skill ${skill.name} ownedCommands must be command tokens`);
169
+ }
170
+ for (const command of skill.ownedCommands) {
171
+ if (commands.has(command)) refuse(`command ${command} is owned by multiple skills`);
172
+ commands.add(command);
173
+ }
174
+ }
175
+ return manifest;
176
+ }
177
+
178
+ /**
179
+ * Read and validate the committed skill-suite manifest. Missing, symlink,
180
+ * malformed, or shape-invalid manifests fail closed.
181
+ *
182
+ * @param {string} repoRoot
183
+ * @returns {{ id: string, version: string, name: string, description: string, skills: Array<{ name: string, path: string, ownedCommands: string[], intent: string }> }}
184
+ */
185
+ export function loadSkillSuiteManifest(repoRoot) {
186
+ const root = resolve(repoRoot);
187
+ const fullPath = assertRegularContainedPath(root, MANIFEST_REL);
188
+ let parsed;
189
+ try {
190
+ parsed = JSON.parse(readFileSync(fullPath, "utf8"));
191
+ } catch {
192
+ refuse("skill suite manifest is malformed");
193
+ }
194
+ return validateSkillSuiteManifest(parsed);
195
+ }
196
+
197
+ function listFilesRecursive(dir, relativeBase = "") {
198
+ const results = [];
199
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
200
+ const rel = relativeBase ? `${relativeBase}/${entry.name}` : entry.name;
201
+ const full = join(dir, entry.name);
202
+ if (entry.isSymbolicLink() || lstatSync(full).isSymbolicLink()) {
203
+ refuse(`symlinks are not allowed: ${rel}`);
204
+ }
205
+ if (entry.isDirectory()) results.push(...listFilesRecursive(full, rel));
206
+ else if (entry.isFile()) results.push(rel);
207
+ else refuse(`unsupported skill artifact: ${rel}`);
208
+ }
209
+ return results.sort();
210
+ }
211
+
212
+ function ownedSkillDir(root, skillName) {
213
+ return assertRegularContainedPath(root, `plugins/kxm/skills/${skillName}`, { directory: true });
214
+ }
215
+
216
+ /**
217
+ * Return generated skill-mirror artifact paths relative to the repo root.
218
+ *
219
+ * @param {string} [repoRoot]
220
+ * @returns {string[]}
221
+ */
222
+ export function listSkillMirrorArtifacts(repoRoot = process.cwd()) {
223
+ const root = resolve(repoRoot);
224
+ const suiteManifest = loadSkillSuiteManifest(root);
225
+ const artifacts = [];
226
+ for (const skill of suiteManifest.skills) {
227
+ const srcSkillDir = ownedSkillDir(root, skill.name);
228
+ const files = listFilesRecursive(srcSkillDir);
229
+ if (!files.includes("SKILL.md")) refuse(`SKILL.md missing for ${skill.name}`);
230
+ for (const rel of files) artifacts.push(`.agents/skills/${skill.name}/${rel}`);
231
+ }
232
+ return artifacts;
233
+ }
234
+
235
+ /**
236
+ * Emit the portable .agents/skills mirror from the authored plugin skills tree
237
+ * and update the AGENTS.md command block. Owned skills are replaced; unrelated
238
+ * skills are preserved. Missing or invalid manifests fail closed.
239
+ *
240
+ * @param {string} [repoRoot]
241
+ */
50
242
  export function emitCodexArtifacts(repoRoot = process.cwd()) {
51
243
  const root = resolve(repoRoot);
52
- const srcSkillsDir = join(root, "plugins", "kxm", "skills");
244
+ const suiteManifest = loadSkillSuiteManifest(root);
53
245
  const destSkillsDir = join(root, ".agents", "skills");
246
+ assertExistingAncestorsAreRegularContained(root, ".agents/skills");
247
+ mkdirSync(destSkillsDir, { recursive: true });
248
+ assertExistingAncestorsAreRegularContained(root, ".agents/skills", { required: true });
249
+ const destRoot = realpathSync(destSkillsDir);
250
+ const repoReal = containedRoot(root);
251
+ const repoPrefix = repoReal.endsWith(sep) ? repoReal : `${repoReal}${sep}`;
252
+ if (destRoot !== repoReal && !destRoot.startsWith(repoPrefix)) {
253
+ refuse("path escapes repository: .agents/skills");
254
+ }
255
+ const ownedSkillNames = suiteManifest.skills.map((skill) => skill.name);
256
+ const emittedSkillPaths = [];
257
+ const preservedSkillNames = [];
54
258
 
55
- // 1. Emit .agents/skills from authored skill tree
56
- if (existsSync(srcSkillsDir)) {
57
- mkdirSync(destSkillsDir, { recursive: true });
58
- cpSync(srcSkillsDir, destSkillsDir, { recursive: true });
259
+ for (const skill of suiteManifest.skills) {
260
+ const srcSkillDir = ownedSkillDir(root, skill.name);
261
+ listFilesRecursive(srcSkillDir);
262
+ const destSkillPath = join(destSkillsDir, skill.name);
263
+ if (existsSync(destSkillPath)) {
264
+ if (lstatSync(destSkillPath).isSymbolicLink()) {
265
+ refuse(`symlinks are not allowed: .agents/skills/${skill.name}`);
266
+ }
267
+ rmSync(destSkillPath, { recursive: true, force: true });
268
+ }
269
+ cpSync(srcSkillDir, destSkillPath, {
270
+ recursive: true,
271
+ dereference: false,
272
+ });
273
+ if (lstatSync(destSkillPath).isSymbolicLink()) {
274
+ rmSync(destSkillPath, { recursive: true, force: true });
275
+ refuse(`symlinks are not allowed: .agents/skills/${skill.name}`);
276
+ }
277
+ const realDest = realpathSync(destSkillPath);
278
+ const prefix = destRoot.endsWith(sep) ? destRoot : `${destRoot}${sep}`;
279
+ if (realDest !== destRoot && !realDest.startsWith(prefix)) {
280
+ rmSync(destSkillPath, { recursive: true, force: true });
281
+ refuse(`path escapes repository: .agents/skills/${skill.name}`);
282
+ }
283
+ listFilesRecursive(destSkillPath);
284
+ emittedSkillPaths.push(destSkillPath);
285
+ }
286
+
287
+ for (const entry of readdirSync(destSkillsDir, { withFileTypes: true })) {
288
+ if (entry.isDirectory() && !ownedSkillNames.includes(entry.name)) {
289
+ preservedSkillNames.push(entry.name);
290
+ }
59
291
  }
60
292
 
61
- // 2. Emit marker-delimited AGENTS.md block
62
293
  const agentsPath = join(root, "AGENTS.md");
63
294
  let agentsUpdated = false;
64
295
  if (existsSync(agentsPath)) {
296
+ if (lstatSync(agentsPath).isSymbolicLink()) refuse("AGENTS.md must be a regular file");
65
297
  const original = readFileSync(agentsPath, "utf8");
66
298
  let updated;
67
299
  if (original.includes(MARKER_START) && original.includes(MARKER_END)) {
@@ -82,10 +314,22 @@ export function emitCodexArtifacts(repoRoot = process.cwd()) {
82
314
  }
83
315
  }
84
316
 
85
- return { destSkillsDir, agentsPath, agentsUpdated };
317
+ return {
318
+ destSkillsDir,
319
+ agentsPath,
320
+ agentsUpdated,
321
+ ownedSkills: ownedSkillNames,
322
+ preservedSkills: preservedSkillNames,
323
+ emittedSkillPaths,
324
+ artifacts: listSkillMirrorArtifacts(root),
325
+ };
86
326
  }
87
327
 
88
328
  if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
89
329
  const result = emitCodexArtifacts();
90
- process.stdout.write(`Codex artifacts emitted: skills -> ${result.destSkillsDir}, AGENTS.md updated -> ${result.agentsUpdated}\n`);
330
+ process.stdout.write(
331
+ `Codex artifacts emitted: skills -> ${result.destSkillsDir}, ` +
332
+ `owned=${result.ownedSkills.length}, preserved=${result.preservedSkills.length}, ` +
333
+ `AGENTS.md updated -> ${result.agentsUpdated}\n`,
334
+ );
91
335
  }
@@ -32,11 +32,19 @@ export function parseJson(text: string): unknown;
32
32
  export function piProviderOf(model?: string): string | undefined;
33
33
  export function piModelId(model?: string): string | undefined;
34
34
  export function piAuthCheckArgs(request: { model: string; role: string }): string[];
35
+ export function resolveLaunch(cliId: string, options?: {
36
+ platform?: NodeJS.Platform | string | undefined;
37
+ pathEnv?: string | undefined;
38
+ existsSync?: ((path: string) => boolean) | undefined;
39
+ realpathSync?: ((path: string) => string) | undefined;
40
+ execPath?: string | undefined;
41
+ }): { command: string; args: string[] };
35
42
  export function resolveLauncher(cliId: string, options?: {
36
- platform?: NodeJS.Platform | string;
37
- pathEnv?: string;
38
- existsSync?: (path: string) => boolean;
39
- realpathSync?: (path: string) => string;
43
+ platform?: NodeJS.Platform | string | undefined;
44
+ pathEnv?: string | undefined;
45
+ existsSync?: ((path: string) => boolean) | undefined;
46
+ realpathSync?: ((path: string) => string) | undefined;
47
+ execPath?: string | undefined;
40
48
  }): string;
41
49
  export function unsupportedLauncherMessage(target: string, platform?: string): string;
42
50
  export function parseAuth(harness: string, stdio: {
@@ -269,7 +269,15 @@ function isAbsoluteOn(path, platform) {
269
269
  return path.startsWith("/");
270
270
  }
271
271
 
272
- export function resolveLauncher(cliId, options = {}) {
272
+ /** Allowlisted npm-global inner launchers, relative to the directory that contains `name.cmd`. */
273
+ export const WIN_NPM_INNER_EXE = Object.freeze({
274
+ claude: Object.freeze(["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"]),
275
+ });
276
+ export const WIN_NPM_INNER_NODE_SCRIPT = Object.freeze({
277
+ pi: Object.freeze(["node_modules", "@earendil-works", "pi-coding-agent", "dist", "bundle", "cli.js"]),
278
+ });
279
+
280
+ export function resolveLaunch(cliId, options = {}) {
273
281
  const platform = options.platform ?? process.platform;
274
282
  const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
275
283
  const exists = options.existsSync ?? existsSync;
@@ -282,25 +290,57 @@ export function resolveLauncher(cliId, options = {}) {
282
290
  if (cliId.includes("\0")) throw failClosed("launcher id is invalid");
283
291
 
284
292
  const explicit = cliId.includes("/") || cliId.includes("\\") || isAbsoluteOn(cliId, platform);
285
- if (explicit) return assertWinExecutable(cliId, platform, options);
293
+ if (explicit) return { command: assertWinExecutable(cliId, platform, options), args: [] };
286
294
 
287
295
  const dirs = pathEnv.split(delim).filter(Boolean);
288
296
  let rejectedShim;
289
- for (const dir of dirs) {
290
- if (platform === "win32") {
291
- const exeName = cliId.toLowerCase().endsWith(".exe") ? cliId : `${cliId}.exe`;
297
+ if (platform === "win32") {
298
+ const exeName = cliId.toLowerCase().endsWith(".exe") ? cliId : `${cliId}.exe`;
299
+ for (const dir of dirs) {
292
300
  const exe = path.join(dir, exeName);
293
- if (exists(exe)) return canonicalize(exe, { ...options, platform });
294
- for (const ext of [".cmd", ".bat", ".ps1", ""]) {
295
- const shim = path.join(dir, `${cliId}${ext}`);
296
- if (exists(shim)) {
297
- rejectedShim = shim;
298
- break;
301
+ if (exists(exe)) return { command: canonicalize(exe, { ...options, platform }), args: [] };
302
+ }
303
+ for (const dir of dirs) {
304
+ const cmdShim = path.join(dir, `${cliId}.cmd`);
305
+ if (!exists(cmdShim)) continue;
306
+ const exeSegs = WIN_NPM_INNER_EXE[cliId];
307
+ if (exeSegs) {
308
+ const inner = path.join(dir, ...exeSegs);
309
+ if (exists(inner)) {
310
+ return { command: assertWinExecutable(inner, platform, options), args: [] };
311
+ }
312
+ }
313
+ const scriptSegs = WIN_NPM_INNER_NODE_SCRIPT[cliId];
314
+ if (scriptSegs) {
315
+ const script = path.join(dir, ...scriptSegs);
316
+ const nodeExe = options.execPath ?? process.execPath;
317
+ if (exists(script) && exists(nodeExe)) {
318
+ return {
319
+ command: canonicalize(nodeExe, { ...options, platform }),
320
+ args: [canonicalize(script, { ...options, platform })],
321
+ };
299
322
  }
300
323
  }
301
- if (rejectedShim) break;
302
- } else if (exists(path.join(dir, cliId))) {
303
- return canonicalize(path.join(dir, cliId), { ...options, platform });
324
+ rejectedShim = cmdShim;
325
+ break;
326
+ }
327
+ if (!rejectedShim) {
328
+ for (const dir of dirs) {
329
+ for (const ext of [".bat", ".ps1", ""]) {
330
+ const shim = path.join(dir, `${cliId}${ext}`);
331
+ if (exists(shim)) {
332
+ rejectedShim = shim;
333
+ break;
334
+ }
335
+ }
336
+ if (rejectedShim) break;
337
+ }
338
+ }
339
+ } else {
340
+ for (const dir of dirs) {
341
+ if (exists(path.join(dir, cliId))) {
342
+ return { command: canonicalize(path.join(dir, cliId), { ...options, platform }), args: [] };
343
+ }
304
344
  }
305
345
  }
306
346
  if (rejectedShim) {
@@ -309,6 +349,10 @@ export function resolveLauncher(cliId, options = {}) {
309
349
  throw failClosed(`missing binary ${cliId}; install and authenticate the native CLI`);
310
350
  }
311
351
 
352
+ export function resolveLauncher(cliId, options = {}) {
353
+ return resolveLaunch(cliId, options).command;
354
+ }
355
+
312
356
  function canonicalize(target, options = {}) {
313
357
  const platform = options.platform ?? process.platform;
314
358
  if (platform === "win32" && process.platform !== "win32") return target;
@@ -510,6 +554,7 @@ export function buildArgv(request, ctx = {}) {
510
554
  "--sandbox",
511
555
  "read-only",
512
556
  "--ignore-user-config",
557
+ "-c", 'approval_policy="never"',
513
558
  ...(schemaPath ? ["--output-schema", schemaPath] : []),
514
559
  "--json",
515
560
  "-",
@@ -1572,19 +1617,22 @@ export async function runHarness(request, deps = {}) {
1572
1617
  schemaText: schemaInput?.text,
1573
1618
  });
1574
1619
  const cliId = request.harness;
1575
- const resolved = resolveLauncher(cliId, {
1620
+ const launch = resolveLaunch(cliId, {
1576
1621
  platform,
1577
1622
  pathEnv: env.PATH,
1578
1623
  existsSync: exists,
1579
1624
  realpathSync: deps.realpathSync,
1625
+ execPath: deps.execPath ?? process.execPath,
1580
1626
  });
1627
+ const resolved = launch.command;
1628
+ const launchArgs = launch.args;
1581
1629
 
1582
1630
  const authArgs = request.harness === "claude"
1583
1631
  ? [...claudeIsolationArgs(mcpConfigPath), ...route.auth.args]
1584
1632
  : request.harness === "pi"
1585
1633
  ? piAuthCheckArgs(request)
1586
1634
  : [...route.auth.args];
1587
- const auth = spawnSyncImpl(resolved, authArgs, {
1635
+ const auth = spawnSyncImpl(resolved, [...launchArgs, ...authArgs], {
1588
1636
  cwd,
1589
1637
  env,
1590
1638
  encoding: "utf8",
@@ -1628,7 +1676,7 @@ export async function runHarness(request, deps = {}) {
1628
1676
  let killRequest;
1629
1677
  let child;
1630
1678
  try {
1631
- child = spawnImpl(resolved, argv, {
1679
+ child = spawnImpl(resolved, [...launchArgs, ...argv], {
1632
1680
  cwd,
1633
1681
  env,
1634
1682
  shell: false,
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Bump the package version across all tracked surfaces.
5
+ *
6
+ * Usage:
7
+ * node scripts/kxm-bump-version.mjs <new-version> # explicit version
8
+ * node scripts/kxm-bump-version.mjs patch # bump patch
9
+ * node scripts/kxm-bump-version.mjs minor # bump minor
10
+ * node scripts/kxm-bump-version.mjs major # bump major
11
+ *
12
+ * Surfaces updated (must stay in sync with scripts/check-versions.mjs):
13
+ * 1. package.json .version
14
+ * 2. package-lock.json .version + .packages[""].version
15
+ * 3. plugins/kxm/package.json .version
16
+ * 4. plugins/kxm/.claude-plugin/plugin.json .version
17
+ * 5. .claude-plugin/marketplace.json .plugins[name=kxm].version
18
+ * 6. plugins/kxm/src/mcp-server.ts const VERSION = "..."
19
+ * 7. plugins/kxm/dist/mcp-server.js const VERSION = "..." (rebuilt)
20
+ *
21
+ * After running this script, run `npm run build` to rebuild dist bundles.
22
+ */
23
+
24
+ import { readFileSync, writeFileSync } from "node:fs";
25
+ import { resolve } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
29
+
30
+ export function parseSemver(version) {
31
+ const match = SEMVER_RE.exec(version);
32
+ if (!match) return undefined;
33
+ return {
34
+ major: Number(match[1]),
35
+ minor: Number(match[2]),
36
+ patch: Number(match[3]),
37
+ prerelease: match[4],
38
+ build: match[5],
39
+ };
40
+ }
41
+
42
+ export function bumpVersion(current, kind) {
43
+ const parsed = parseSemver(current);
44
+ if (!parsed) throw new Error(`invalid current version: ${current}`);
45
+ switch (kind) {
46
+ case "major":
47
+ return `${parsed.major + 1}.0.0`;
48
+ case "minor":
49
+ return `${parsed.major}.${parsed.minor + 1}.0`;
50
+ case "patch":
51
+ return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
52
+ default:
53
+ throw new Error(`unknown bump kind: ${kind} (use major, minor, or patch)`);
54
+ }
55
+ }
56
+
57
+ /** Replace a version string in a JSON file using targeted string patching. */
58
+ function patchJsonVersion(filePath, oldVersion, newVersion) {
59
+ const raw = readFileSync(filePath, "utf8");
60
+ const escaped = oldVersion.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61
+ const pattern = new RegExp(`("version"\\s*:\\s*)"${escaped}"`, "g");
62
+ const patched = raw.replace(pattern, `$1"${newVersion}"`);
63
+ if (patched === raw) {
64
+ throw new Error(`no version field found to patch in ${filePath}`);
65
+ }
66
+ writeFileSync(filePath, patched, "utf8");
67
+ }
68
+
69
+ /** Replace a version string in a TypeScript/JavaScript source file. */
70
+ function patchSourceVersion(filePath, newVersion) {
71
+ const raw = readFileSync(filePath, "utf8");
72
+ const patched = raw.replace(
73
+ /const VERSION = "[^"]+"/,
74
+ `const VERSION = "${newVersion}"`,
75
+ );
76
+ if (patched === raw) {
77
+ throw new Error(`no VERSION constant found in ${filePath}`);
78
+ }
79
+ writeFileSync(filePath, patched, "utf8");
80
+ }
81
+
82
+ export function applyVersionBump(root, newVersion) {
83
+ if (!parseSemver(newVersion)) {
84
+ throw new Error(`invalid semver: ${newVersion}`);
85
+ }
86
+
87
+ // Read current version
88
+ const pkgPath = resolve(root, "package.json");
89
+ const oldVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version;
90
+
91
+ // JSON surfaces (targeted patch preserves formatting)
92
+ const jsonFiles = [
93
+ "package.json",
94
+ "package-lock.json",
95
+ "plugins/kxm/package.json",
96
+ "plugins/kxm/.claude-plugin/plugin.json",
97
+ ".claude-plugin/marketplace.json",
98
+ ];
99
+ for (const rel of jsonFiles) {
100
+ patchJsonVersion(resolve(root, rel), oldVersion, newVersion);
101
+ }
102
+
103
+ // TypeScript source
104
+ patchSourceVersion(resolve(root, "plugins/kxm/src/mcp-server.ts"), newVersion);
105
+
106
+ // Generated dist (update in place if it exists; check:generated rebuilds anyway)
107
+ const distPath = resolve(root, "plugins/kxm/dist/mcp-server.js");
108
+ try {
109
+ patchSourceVersion(distPath, newVersion);
110
+ } catch {
111
+ // dist may not exist yet; will be rebuilt by npm run build
112
+ }
113
+
114
+ return { oldVersion, newVersion };
115
+ }
116
+
117
+ export function main(argv = process.argv.slice(2), root = process.cwd()) {
118
+ const arg = argv[0];
119
+ if (!arg) {
120
+ process.stderr.write("usage: node scripts/kxm-bump-version.mjs <version|patch|minor|major>\n");
121
+ return 1;
122
+ }
123
+
124
+ const pkgPath = resolve(root, "package.json");
125
+ const currentVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version;
126
+
127
+ let newVersion;
128
+ if (SEMVER_RE.test(arg)) {
129
+ newVersion = arg;
130
+ } else {
131
+ newVersion = bumpVersion(currentVersion, arg);
132
+ }
133
+
134
+ if (newVersion === currentVersion) {
135
+ process.stdout.write(`version unchanged: ${currentVersion}\n`);
136
+ return 0;
137
+ }
138
+
139
+ const result = applyVersionBump(root, newVersion);
140
+ process.stdout.write(`bumped ${result.oldVersion} -> ${result.newVersion}\n`);
141
+ return 0;
142
+ }
143
+
144
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
145
+ process.exitCode = main();
146
+ }