@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.
Files changed (117) 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 +5 -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 +2 -0
  14. package/docs/agent-skills.md +118 -0
  15. package/docs/architecture.md +1 -1
  16. package/docs/assignment-runner.md +21 -8
  17. package/docs/configuration.md +2 -2
  18. package/docs/kxm-handbook.md +3 -3
  19. package/docs/operator-pi-packages.md +63 -0
  20. package/docs/skills/repo-work-delivery.md +102 -0
  21. package/docs/test-matrix.md +4 -3
  22. package/docs/troubleshooting.md +19 -0
  23. package/docs/vnext/validation.md +9 -0
  24. package/docs/webhook-workflows.md +2 -2
  25. package/examples/README.md +1 -1
  26. package/package.json +16 -17
  27. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  28. package/plugins/kxm/README.md +1 -1
  29. package/plugins/kxm/dist/cli.js +6875 -3672
  30. package/plugins/kxm/dist/core.js +214 -34
  31. package/plugins/kxm/dist/extension.js +7721 -85
  32. package/plugins/kxm/dist/mcp-server.js +75 -21
  33. package/plugins/kxm/dist/runtime.js +5403 -1014
  34. package/plugins/kxm/dist/server.js +3008 -2268
  35. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
  36. package/plugins/kxm/package.json +1 -1
  37. package/plugins/kxm/skills/SUITE.md +5 -0
  38. package/plugins/kxm/skills/hints.json +73 -0
  39. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  40. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  41. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  42. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  43. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  44. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +34 -0
  45. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  46. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  47. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  48. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +35 -0
  49. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  50. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  51. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  52. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  53. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  54. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  55. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  56. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  57. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  58. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  59. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  60. package/plugins/kxm/src/autocomplete.ts +9 -3
  61. package/plugins/kxm/src/cli.ts +1077 -11
  62. package/plugins/kxm/src/commands.ts +150 -8
  63. package/plugins/kxm/src/config.ts +7 -4
  64. package/plugins/kxm/src/context-packet.ts +172 -0
  65. package/plugins/kxm/src/extension.ts +36 -1
  66. package/plugins/kxm/src/external-effects.ts +356 -7
  67. package/plugins/kxm/src/hub.ts +2 -4
  68. package/plugins/kxm/src/improve.ts +72 -0
  69. package/plugins/kxm/src/mcp-server.ts +1 -1
  70. package/plugins/kxm/src/model-inventory.ts +127 -0
  71. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  72. package/plugins/kxm/src/policy-draft.mjs +565 -0
  73. package/plugins/kxm/src/price-calc.ts +17 -18
  74. package/plugins/kxm/src/prices.ts +32 -16
  75. package/plugins/kxm/src/producers.ts +71 -0
  76. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  77. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  78. package/plugins/kxm/src/role.ts +375 -0
  79. package/plugins/kxm/src/routing.ts +99 -1
  80. package/plugins/kxm/src/session-work.ts +9 -2
  81. package/plugins/kxm/src/studio-layout.ts +660 -17
  82. package/plugins/kxm/src/suggest.ts +7 -13
  83. package/plugins/kxm/src/telemetry.ts +82 -0
  84. package/plugins/kxm/src/tui.ts +140 -0
  85. package/plugins/kxm/src/vnext-config.ts +15 -110
  86. package/plugins/kxm/src/vnext-engine.ts +198 -62
  87. package/plugins/kxm/src/vnext-harness.ts +263 -81
  88. package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
  89. package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
  90. package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
  91. package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
  92. package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
  93. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  94. package/plugins/kxm/src/workflow-manager.ts +392 -0
  95. package/plugins/kxm/src/workflow-tui.ts +255 -0
  96. package/schemas/policy-draft/README.md +17 -0
  97. package/schemas/policy-draft/model.v2.schema.json +140 -0
  98. package/schemas/policy-draft/role.v2.schema.json +91 -0
  99. package/schemas/vnext/role.schema.json +76 -0
  100. package/schemas/vnext/run-event.schema.json +1 -0
  101. package/scripts/assignment-run.d.mts +1 -1
  102. package/scripts/assignment-run.mjs +44 -35
  103. package/scripts/check-generated.mjs +33 -9
  104. package/scripts/emit-codex-artifacts.mjs +255 -11
  105. package/scripts/harness-run.d.mts +12 -4
  106. package/scripts/harness-run.mjs +65 -17
  107. package/scripts/kxm-hub.mjs +6 -0
  108. package/scripts/native-critic.d.mts +5 -0
  109. package/scripts/native-critic.mjs +60 -0
  110. package/.kxm/config/README.md +0 -5
  111. package/.kxm/config/agents.json +0 -43
  112. package/.kxm/config/env.example +0 -56
  113. package/.kxm/config/update.example.yaml +0 -9
  114. package/.kxm/config/workflows/fix.json +0 -160
  115. package/.kxm/config/workflows/jira-development.json +0 -116
  116. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  117. 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
- let cachedDiskPolicy = null;
750
- export function getRosterPolicy(deps = {}) {
751
- if (deps.rosterPolicy) {
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 deps.loadTrustedRosterPolicy === "function") {
755
- const loaded = deps.loadTrustedRosterPolicy();
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
- return loadTrustedRosterPolicy().policy;
760
- } catch (error) {
761
- // When executing in a local development checkout where git status has dirty
762
- // uncommitted edits or during unit tests running against in-flight code,
763
- // fallback to parsing the local .kxm/roster.json.
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 cachedDiskPolicy;
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
- rosterPolicy: deps.rosterPolicy,
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
- return REQUIRED_ACCEPT_CRITICS;
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
- if (policy?.lineup?.writer && policy?.routes) {
3413
- const matched = findLineupRoute(policy, "writer", bound.identity.harness, bound.identity.model);
3414
- if (!matched) {
3415
- throw failClosed(`writer ${bound.identity.harness}/${bound.identity.model} is not admitted in lineup`, "witness_binding_invalid");
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
- let policy;
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 GENERATED_ARTIFACTS = [
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 missing = GENERATED_ARTIFACTS.filter((path) => !existsSync(resolve(root, path)));
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 = GENERATED_ARTIFACTS.filter((path) => runGit(
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
- ...GENERATED_ARTIFACTS,
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: [...GENERATED_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 { 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: {