@kontextmind/kxm 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +5 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +2 -0
- package/docs/agent-skills.md +118 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +2 -2
- package/docs/kxm-handbook.md +3 -3
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +102 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +19 -0
- 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 +6875 -3672
- package/plugins/kxm/dist/core.js +214 -34
- package/plugins/kxm/dist/extension.js +7721 -85
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5403 -1014
- package/plugins/kxm/dist/server.js +3008 -2268
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
- 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 +34 -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 +35 -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 +1077 -11
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +356 -7
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- 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/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +375 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/session-work.ts +9 -2
- 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-config.ts +15 -110
- package/plugins/kxm/src/vnext-engine.ts +198 -62
- package/plugins/kxm/src/vnext-harness.ts +263 -81
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
- package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
- 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/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-hub.mjs +6 -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
|
@@ -746,30 +746,35 @@ function validateRework(reworkOf, taskDir, taskId, assignmentId, deps) {
|
|
|
746
746
|
return priorId;
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
return deps.rosterPolicy.policy ?? deps.rosterPolicy;
|
|
749
|
+
function requireRosterPolicyShape(policy) {
|
|
750
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
|
|
751
|
+
throw failClosed("roster policy is invalid or unparsed", "route_invalid");
|
|
753
752
|
}
|
|
754
|
-
if (typeof
|
|
755
|
-
|
|
756
|
-
return loaded?.policy ?? loaded;
|
|
753
|
+
if (!policy.routes || typeof policy.routes !== "object" || Array.isArray(policy.routes) || Object.keys(policy.routes).length === 0) {
|
|
754
|
+
throw failClosed("roster policy is invalid or unparsed", "route_invalid");
|
|
757
755
|
}
|
|
756
|
+
if (!policy.lineup || typeof policy.lineup !== "object" || Array.isArray(policy.lineup)) {
|
|
757
|
+
throw failClosed("roster policy is invalid or unparsed", "route_invalid");
|
|
758
|
+
}
|
|
759
|
+
return policy;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function unwrapRosterPolicy(loaded) {
|
|
763
|
+
return requireRosterPolicyShape(loaded?.policy ?? loaded);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function getRosterPolicy(deps = {}) {
|
|
758
767
|
try {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (!cachedDiskPolicy) {
|
|
765
|
-
try {
|
|
766
|
-
const diskPath = join(fileURLToPath(new URL(".", import.meta.url)), "..", ".kxm/roster.json");
|
|
767
|
-
cachedDiskPolicy = Object.freeze(JSON.parse(readFileSync(diskPath, "utf8")));
|
|
768
|
-
} catch {
|
|
769
|
-
throw failClosed(error?.message ?? "roster policy unavailable", "route_invalid");
|
|
770
|
-
}
|
|
768
|
+
if (Object.hasOwn(deps, "rosterPolicy")) {
|
|
769
|
+
return unwrapRosterPolicy(deps.rosterPolicy);
|
|
770
|
+
}
|
|
771
|
+
if (typeof deps.loadTrustedRosterPolicy === "function") {
|
|
772
|
+
return unwrapRosterPolicy(deps.loadTrustedRosterPolicy());
|
|
771
773
|
}
|
|
772
|
-
return
|
|
774
|
+
return unwrapRosterPolicy(loadTrustedRosterPolicy());
|
|
775
|
+
} catch (error) {
|
|
776
|
+
if (error?.runnerCode === "route_invalid") throw error;
|
|
777
|
+
throw failClosed(error?.message ?? "roster policy unavailable", "route_invalid");
|
|
773
778
|
}
|
|
774
779
|
}
|
|
775
780
|
|
|
@@ -979,6 +984,15 @@ const MODEL_CLAIM_OVERRIDE_KEYS = Object.freeze([
|
|
|
979
984
|
"usage",
|
|
980
985
|
"transport",
|
|
981
986
|
]);
|
|
987
|
+
function injectedPolicyDeps(source = {}) {
|
|
988
|
+
const deps = {};
|
|
989
|
+
if (Object.hasOwn(source, "rosterPolicy")) deps.rosterPolicy = source.rosterPolicy;
|
|
990
|
+
if (typeof source.loadTrustedRosterPolicy === "function") {
|
|
991
|
+
deps.loadTrustedRosterPolicy = source.loadTrustedRosterPolicy;
|
|
992
|
+
}
|
|
993
|
+
return deps;
|
|
994
|
+
}
|
|
995
|
+
|
|
982
996
|
function ioDeps(deps = {}) {
|
|
983
997
|
return {
|
|
984
998
|
spawnSync: deps.spawnSync ?? spawnSync,
|
|
@@ -996,8 +1010,7 @@ function ioDeps(deps = {}) {
|
|
|
996
1010
|
rmSync: deps.rmSync ?? rmSync,
|
|
997
1011
|
manifestBytes: deps.manifestBytes,
|
|
998
1012
|
now: deps.now,
|
|
999
|
-
|
|
1000
|
-
loadTrustedRosterPolicy: deps.loadTrustedRosterPolicy,
|
|
1013
|
+
...injectedPolicyDeps(deps),
|
|
1001
1014
|
};
|
|
1002
1015
|
}
|
|
1003
1016
|
|
|
@@ -3374,7 +3387,7 @@ function closedObserved(pr, ci) {
|
|
|
3374
3387
|
|
|
3375
3388
|
export function resolveRequiredCritics(policy) {
|
|
3376
3389
|
if (!policy || typeof policy !== "object" || !policy.required_critics || !policy.routes) {
|
|
3377
|
-
|
|
3390
|
+
throw failClosed("roster policy is invalid or unparsed", "route_invalid");
|
|
3378
3391
|
}
|
|
3379
3392
|
const result = {};
|
|
3380
3393
|
for (const [kind, routeId] of Object.entries(policy.required_critics)) {
|
|
@@ -3409,11 +3422,10 @@ function assertEligibleWriter(bound, policy) {
|
|
|
3409
3422
|
if (!(transport?.status === "completed" && transport?.ok === true)) {
|
|
3410
3423
|
throw failClosed("writer transport is not completed/ok", "witness_binding_invalid");
|
|
3411
3424
|
}
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
}
|
|
3425
|
+
requireRosterPolicyShape(policy);
|
|
3426
|
+
const matched = findLineupRoute(policy, "writer", bound.identity.harness, bound.identity.model);
|
|
3427
|
+
if (!matched) {
|
|
3428
|
+
throw failClosed(`writer ${bound.identity.harness}/${bound.identity.model} is not admitted in lineup`, "witness_binding_invalid");
|
|
3417
3429
|
}
|
|
3418
3430
|
}
|
|
3419
3431
|
|
|
@@ -3669,12 +3681,7 @@ export async function acceptAssignment(request, deps = {}) {
|
|
|
3669
3681
|
const commitValue = request?.commit;
|
|
3670
3682
|
const writerRecordValue = request?.recordDir ?? request?.record_dir;
|
|
3671
3683
|
const criticValues = request?.critics;
|
|
3672
|
-
|
|
3673
|
-
try {
|
|
3674
|
-
policy = getRosterPolicy(deps);
|
|
3675
|
-
} catch {
|
|
3676
|
-
policy = null;
|
|
3677
|
-
}
|
|
3684
|
+
const policy = getRosterPolicy(deps);
|
|
3678
3685
|
const criticSpecs = resolveRequiredCritics(policy);
|
|
3679
3686
|
const requiredKinds = Object.keys(criticSpecs);
|
|
3680
3687
|
if (!Array.isArray(criticValues) || criticValues.length !== requiredKinds.length) {
|
|
@@ -4337,6 +4344,7 @@ export async function main(argv = process.argv, io = { stdin: process.stdin, std
|
|
|
4337
4344
|
spawnSync: io.spawnSync ?? spawnSync,
|
|
4338
4345
|
env: io.env,
|
|
4339
4346
|
now: io.now,
|
|
4347
|
+
...injectedPolicyDeps(io),
|
|
4340
4348
|
});
|
|
4341
4349
|
io.stdout.write(`${JSON.stringify(accepted, undefined, 2)}\n`);
|
|
4342
4350
|
process.exitCode = 0;
|
|
@@ -4395,6 +4403,7 @@ export async function main(argv = process.argv, io = { stdin: process.stdin, std
|
|
|
4395
4403
|
spawnSync: io.spawnSync ?? spawnSync,
|
|
4396
4404
|
env: io.env,
|
|
4397
4405
|
now: io.now,
|
|
4406
|
+
...injectedPolicyDeps(io),
|
|
4398
4407
|
});
|
|
4399
4408
|
io.stdout.write(`${JSON.stringify(receipt, undefined, 2)}\n`);
|
|
4400
4409
|
process.exitCode = receipt.result === "passed" ? 0 : 1;
|
|
@@ -4418,7 +4427,7 @@ export async function main(argv = process.argv, io = { stdin: process.stdin, std
|
|
|
4418
4427
|
const bytes = readFileSync(manifestPath);
|
|
4419
4428
|
const manifest = parseJsonFile(manifestPath, bytes, "assignment manifest");
|
|
4420
4429
|
try {
|
|
4421
|
-
const completion = await runAssignment(manifest, { manifestBytes: bytes });
|
|
4430
|
+
const completion = await runAssignment(manifest, { manifestBytes: bytes, ...injectedPolicyDeps(io) });
|
|
4422
4431
|
io.stdout.write(`${JSON.stringify(completion, undefined, 2)}\n`);
|
|
4423
4432
|
process.exitCode = completion.transport?.ok === true && completion.recording?.status === "ok" ? 0 : 1;
|
|
4424
4433
|
return completion;
|
|
@@ -4,8 +4,9 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { listSkillMirrorArtifacts, loadSkillSuiteManifest } from "./emit-codex-artifacts.mjs";
|
|
7
8
|
|
|
8
|
-
export const
|
|
9
|
+
export const STATIC_GENERATED_ARTIFACTS = Object.freeze([
|
|
9
10
|
"plugins/kxm/dist/cli.js",
|
|
10
11
|
"plugins/kxm/dist/server.js",
|
|
11
12
|
"plugins/kxm/dist/mcp-server.js",
|
|
@@ -14,13 +15,34 @@ export const GENERATED_ARTIFACTS = [
|
|
|
14
15
|
"plugins/kxm/dist/runtime.js",
|
|
15
16
|
"plugins/kxm/dist/client.js",
|
|
16
17
|
"plugins/kxm/dist/extension.js",
|
|
17
|
-
".agents/skills/kxm/SKILL.md",
|
|
18
|
-
".agents/skills/kxm/references/protocol.md",
|
|
19
|
-
".agents/skills/kxm-session/SKILL.md",
|
|
20
18
|
"AGENTS.md",
|
|
21
19
|
"CLAUDE.md",
|
|
22
20
|
"GEMINI.md",
|
|
23
|
-
];
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Compute the generated artifact list from the committed suite manifest.
|
|
25
|
+
* The manifest is required; missing or invalid manifests fail closed.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} [repository]
|
|
28
|
+
* @returns {string[]}
|
|
29
|
+
*/
|
|
30
|
+
export function computeGeneratedArtifacts(repository = process.cwd()) {
|
|
31
|
+
const root = resolve(repository);
|
|
32
|
+
loadSkillSuiteManifest(root);
|
|
33
|
+
return Object.freeze([
|
|
34
|
+
...STATIC_GENERATED_ARTIFACTS,
|
|
35
|
+
...listSkillMirrorArtifacts(root),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Synchronous snapshot for the current repository. Tests that need a
|
|
41
|
+
* fixture-specific list should call computeGeneratedArtifacts(root).
|
|
42
|
+
*
|
|
43
|
+
* @type {readonly string[]}
|
|
44
|
+
*/
|
|
45
|
+
export const GENERATED_ARTIFACTS = computeGeneratedArtifacts();
|
|
24
46
|
|
|
25
47
|
function runGit(repository, args, allowFailure = false) {
|
|
26
48
|
const result = spawnSync("git", args, {
|
|
@@ -39,12 +61,14 @@ export function checkGeneratedArtifacts(repository = process.cwd()) {
|
|
|
39
61
|
const requestedRoot = resolve(repository);
|
|
40
62
|
const topLevel = runGit(requestedRoot, ["rev-parse", "--show-toplevel"]).stdout.trim();
|
|
41
63
|
const root = resolve(topLevel);
|
|
42
|
-
const
|
|
64
|
+
const artifacts = computeGeneratedArtifacts(root);
|
|
65
|
+
|
|
66
|
+
const missing = artifacts.filter((path) => !existsSync(resolve(root, path)));
|
|
43
67
|
if (missing.length > 0) {
|
|
44
68
|
throw new Error(`generated artifacts are missing: ${missing.join(", ")}`);
|
|
45
69
|
}
|
|
46
70
|
|
|
47
|
-
const untracked =
|
|
71
|
+
const untracked = artifacts.filter((path) => runGit(
|
|
48
72
|
root,
|
|
49
73
|
["--literal-pathspecs", "ls-files", "--error-unmatch", "--", path],
|
|
50
74
|
true,
|
|
@@ -58,12 +82,12 @@ export function checkGeneratedArtifacts(repository = process.cwd()) {
|
|
|
58
82
|
"diff",
|
|
59
83
|
"--name-only",
|
|
60
84
|
"--",
|
|
61
|
-
...
|
|
85
|
+
...artifacts,
|
|
62
86
|
]).stdout.trim();
|
|
63
87
|
if (status) {
|
|
64
88
|
throw new Error(`generated artifacts changed after build:\n${status}`);
|
|
65
89
|
}
|
|
66
|
-
return { root, artifacts: [...
|
|
90
|
+
return { root, artifacts: [...artifacts] };
|
|
67
91
|
}
|
|
68
92
|
|
|
69
93
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
@@ -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: {
|