@kontextmind/kxm 0.6.0 → 0.7.4
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.kxm/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +7 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +4 -0
- package/docs/agent-skills.md +121 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +21 -0
- package/docs/kxm-handbook.md +3 -3
- package/docs/operations.md +24 -0
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +107 -0
- package/docs/skills.md +2 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +41 -1
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +8955 -3934
- package/plugins/kxm/dist/core.js +271 -34
- package/plugins/kxm/dist/extension.js +7759 -86
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5637 -1125
- package/plugins/kxm/dist/server.js +3125 -2260
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5808 -565
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +73 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- package/plugins/kxm/src/cli.ts +1637 -73
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +357 -8
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +710 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +53 -111
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +214 -62
- package/plugins/kxm/src/vnext-harness.ts +267 -83
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +166 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +179 -224
- package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/plugins/kxm/src/workflow.ts +144 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +150 -2
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- package/.kxm/config/workflows/v04-dogfood.json +0 -72
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
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> <
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 {
|
|
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(
|
|
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: {
|
package/scripts/harness-run.mjs
CHANGED
|
@@ -269,7 +269,15 @@ function isAbsoluteOn(path, platform) {
|
|
|
269
269
|
return path.startsWith("/");
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
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
|
+
}
|