@kontextmind/kxm 0.7.130 → 0.7.132

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 (54) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/README.md +4 -1
  3. package/.kxm/agents/coordinator.yaml +1 -0
  4. package/.kxm/agents/critic-arch.yaml +1 -4
  5. package/.kxm/agents/critic-cli.yaml +1 -4
  6. package/.kxm/agents/implementer.yaml +1 -4
  7. package/.kxm/workflows/implement-only.yaml +1 -1
  8. package/.kxm/workflows/review-arch-only.yaml +1 -1
  9. package/.kxm/workflows/review-cli-only.yaml +1 -1
  10. package/CHANGELOG.md +22 -1
  11. package/docs/concepts/architecture.md +1 -1
  12. package/docs/contracts/routing.md +3 -3
  13. package/docs/contributing/assignment-runner.md +14 -15
  14. package/docs/contributing/development.md +19 -5
  15. package/docs/contributing/harness-routing-internals.md +8 -8
  16. package/docs/glossary.md +1 -1
  17. package/docs/guides/agent-skills.md +1 -1
  18. package/docs/operations/backup-and-restore.md +4 -4
  19. package/docs/reference/cli-reference.md +26 -8
  20. package/docs/reference/config-reference.md +76 -153
  21. package/docs/reference/harness-routing.md +29 -21
  22. package/docs/reference/workflow-catalog.md +4 -4
  23. package/docs/start/first-workflow.md +1 -1
  24. package/examples/project/.kxm/agents/coordinator.yaml +1 -2
  25. package/examples/project/.kxm/agents/critic-1.yaml +1 -3
  26. package/examples/project/.kxm/agents/critic-2.yaml +1 -2
  27. package/examples/project/.kxm/agents/critic-3.yaml +2 -3
  28. package/examples/project/.kxm/agents/implementer.yaml +1 -2
  29. package/examples/project/.kxm/agents/planner.yaml +1 -2
  30. package/examples/project/.kxm/agents/reproducer.yaml +1 -2
  31. package/examples/project/.kxm/agents/reviewer.yaml +1 -2
  32. package/examples/project/.kxm/workflows/fix.yaml +0 -12
  33. package/package.json +1 -1
  34. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  35. package/plugins/kxm/dist/cli.js +287 -249
  36. package/plugins/kxm/dist/mcp-server.js +1 -1
  37. package/plugins/kxm/dist/runtime-supervisor.js +222 -242
  38. package/plugins/kxm/dist/runtime.js +224 -244
  39. package/plugins/kxm/package.json +1 -1
  40. package/plugins/kxm/skills/kxm/SKILL.md +1 -1
  41. package/plugins/kxm/skills/kxm-definitions/SKILL.md +8 -0
  42. package/plugins/kxm/src/cli/project.ts +4 -4
  43. package/plugins/kxm/src/engine.ts +168 -163
  44. package/plugins/kxm/src/init-guide-setup.ts +75 -15
  45. package/plugins/kxm/src/mcp-server.ts +1 -1
  46. package/plugins/kxm/src/oneshot-producer.ts +3 -15
  47. package/plugins/kxm/src/project-config.ts +83 -70
  48. package/plugins/kxm/src/routes.ts +26 -18
  49. package/plugins/kxm/src/runtime-supervisor.ts +0 -18
  50. package/plugins/kxm/src/suggest.ts +2 -2
  51. package/plugins/kxm/src/template.ts +2 -14
  52. package/schemas/agent.schema.json +8 -0
  53. package/scripts/roster-policy.d.mts +2 -1
  54. package/scripts/roster-policy.mjs +119 -22
@@ -8,9 +8,11 @@ import YAML from 'yaml';
8
8
  import { NATIVE_PI_BRAKE_PROVIDERS, PI_ALLOWED_PROVIDERS, PI_ANTIGRAVITY_MODEL_ID, PI_NATIVE_VENDOR_PROVIDERS, ROUTES } from './harness-run.mjs';
9
9
 
10
10
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
- const POLICY = '.kxm/roster.yaml';
12
- const RETIRED_POLICY = '.kxm/roster.json';
11
+ const MODEL_DIR = '.kxm/models';
12
+ const ROLE_DIR = '.kxm/roles';
13
+ const RETIRED_ROSTER_FILES = ['.kxm/roster.json', '.kxm/roster.yaml'];
13
14
  const TRUSTED = 'refs/remotes/origin/main';
15
+ const SKIP_MODELS = new Set(['inventory.yaml']);
14
16
  const ROLES = ['writer', 'planner', 'reviewer-arch', 'reviewer-cli', 'experiment'];
15
17
  const ALIASES = Object.freeze({ 'x-ai': 'xai', moonshotai: 'moonshot', 'google-ai': 'google', qwen: 'alibaba' });
16
18
  const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
@@ -55,14 +57,19 @@ function frozen(value) {
55
57
  if (value && typeof value === 'object') { for (const child of Object.values(value)) frozen(child); Object.freeze(value); }
56
58
  return value;
57
59
  }
60
+ export function refuseRetiredRosterFile(root) {
61
+ for (const retired of RETIRED_ROSTER_FILES) {
62
+ if (existsSync(path.join(root, retired))) refuse(`retired_roster_file: retired ${retired} present; use ${MODEL_DIR} and ${ROLE_DIR}`);
63
+ }
64
+ }
58
65
  function control() {
59
- if (existsSync(path.join(ROOT, RETIRED_POLICY))) refuse(`retired ${RETIRED_POLICY} present; use ${POLICY}`);
66
+ refuseRetiredRosterFile(ROOT);
60
67
  if (realpathSync(gitText('rev-parse', '--show-toplevel')) !== realpathSync(ROOT)) refuse('module is outside its control repository');
61
68
  const head = gitText('rev-parse', '--verify', 'HEAD^{commit}');
62
69
  const trusted = gitText('rev-parse', '--verify', `${TRUSTED}^{commit}`);
63
70
  git('merge-base', '--is-ancestor', head, trusted);
64
71
  const flags = git('ls-files', '-v', '-z').toString('utf8').split('\0').filter(Boolean);
65
- if (flags.some(entry => entry.slice(2) !== POLICY && (entry[0] === 'S' || entry[0] === entry[0].toLowerCase()))) refuse('hidden index flags on control source');
72
+ if (flags.some(entry => entry[0] === 'S' || entry[0] === entry[0].toLowerCase())) refuse('hidden index flags on control source');
66
73
  if (git('status', '--porcelain=v1', '--untracked-files=all').length) refuse('dirty or untracked control source');
67
74
  return { head, trusted };
68
75
  }
@@ -88,8 +95,7 @@ function workingBytes(source) {
88
95
  return readFileSync(current);
89
96
  }
90
97
  export function validateRosterDocument(policy, readBlob) {
91
- keys(policy, ['schema', 'routes', 'lineup', 'required_critics', 'model_origins'], 'policy');
92
- if (policy.schema !== 'kxm.developer-roster.v1') refuse('unsupported schema');
98
+ keys(policy, ['routes', 'lineup', 'required_critics', 'model_origins'], 'policy');
93
99
  record(policy.routes, 'routes'); record(policy.lineup, 'lineup'); record(policy.model_origins, 'model origins');
94
100
  if (!Object.keys(policy.routes).length) refuse('empty routes');
95
101
  if (typeof readBlob !== 'function') refuse('origin evidence reader required');
@@ -162,35 +168,126 @@ export function validateRosterDocument(policy, readBlob) {
162
168
  }
163
169
  return policy;
164
170
  }
165
- function validate(bytes, commit, trusted) {
166
- let policy;
167
- try { policy = YAML.parse(bytes.toString('utf8')); } catch { refuse('invalid policy YAML'); }
168
- if (policy === undefined || policy === null || typeof policy !== 'object' || Array.isArray(policy)) refuse('invalid policy YAML');
171
+ function parsePolicyYaml(bytes, label) {
172
+ let value;
173
+ try { value = YAML.parse(bytes.toString('utf8')); } catch { refuse(`invalid policy YAML (${label})`); }
174
+ if (value === undefined || value === null || typeof value !== 'object' || Array.isArray(value)) refuse(`invalid policy YAML (${label})`);
175
+ return value;
176
+ }
177
+ function listedPolicyFiles(commit) {
178
+ const raw = git('ls-tree', '-r', '-z', commit, '--', MODEL_DIR, ROLE_DIR).toString('utf8');
179
+ const files = [];
180
+ for (const entry of raw.split('\0').filter(Boolean)) {
181
+ const match = /^(100644|100755) blob ([a-f0-9]{40}(?:[a-f0-9]{24})?)\t(.+)$/u.exec(entry);
182
+ if (!match) refuse('source is missing or not a regular committed file');
183
+ const source = match[3];
184
+ if (!source.endsWith('.yaml')) continue;
185
+ const name = source.slice(source.lastIndexOf('/') + 1);
186
+ if (source.startsWith(`${MODEL_DIR}/`) && SKIP_MODELS.has(name)) continue;
187
+ if (source.startsWith(`${MODEL_DIR}/`) || source.startsWith(`${ROLE_DIR}/`)) files.push(source);
188
+ }
189
+ if (!files.some(source => source.startsWith(`${ROLE_DIR}/`)) || !files.some(source => source.startsWith(`${MODEL_DIR}/`))) {
190
+ refuse('source is missing or not a regular committed file');
191
+ }
192
+ return files.sort();
193
+ }
194
+ /** Build the runner policy object from parsed model and role documents. */
195
+ export function assembleRosterPolicy(models, roles) {
196
+ if (!Array.isArray(models) || !Array.isArray(roles)) refuse('invalid policy YAML');
197
+ const lineup = {};
198
+ const rolesByRoute = {};
199
+ for (const role of [...roles].sort((left, right) => String(left?.id).localeCompare(String(right?.id)))) {
200
+ record(role, 'role');
201
+ text(role.id, 'role id');
202
+ if (!Array.isArray(role.roster)) refuse('lineup contains unsupported or duplicate values');
203
+ const ids = [];
204
+ for (const entry of role.roster) {
205
+ record(entry, 'roster entry');
206
+ text(entry.route, 'roster route');
207
+ ids.push(entry.route);
208
+ if (!rolesByRoute[entry.route]) rolesByRoute[entry.route] = [];
209
+ if (!rolesByRoute[entry.route].includes(role.id)) rolesByRoute[entry.route].push(role.id);
210
+ }
211
+ lineup[role.id] = ids;
212
+ }
213
+ const routes = {};
214
+ const model_origins = {};
215
+ for (const doc of models) {
216
+ record(doc, 'model');
217
+ text(doc.id, 'route id');
218
+ if (!rolesByRoute[doc.id]) continue;
219
+ routes[doc.id] = {
220
+ harness: doc.harness,
221
+ model: doc.model,
222
+ vendor: doc.vendor,
223
+ roles: rolesByRoute[doc.id],
224
+ permissions: doc.permissions,
225
+ status: doc.status,
226
+ };
227
+ if (doc.origin !== undefined) {
228
+ record(doc.origin, 'origin');
229
+ const evidence = { source: doc.origin.source, sha256: doc.origin.sha256 };
230
+ if (doc.origin.commit !== undefined) evidence.commit = doc.origin.commit;
231
+ model_origins[doc.model] = { vendor: doc.vendor, evidence };
232
+ }
233
+ }
234
+ return {
235
+ routes,
236
+ lineup,
237
+ required_critics: {
238
+ 'review-arch': lineup['reviewer-arch']?.[0],
239
+ 'review-cli': lineup['reviewer-cli']?.[0],
240
+ },
241
+ model_origins,
242
+ };
243
+ }
244
+ function readPolicySet(commit, checkWorking) {
245
+ const files = listedPolicyFiles(commit);
246
+ const models = [];
247
+ const roles = [];
248
+ const parts = [];
249
+ for (const source of files) {
250
+ const { bytes } = blobAt(commit, source);
251
+ if (checkWorking && !workingBytes(source).equals(bytes)) refuse('working policy differs from committed bytes');
252
+ const doc = parsePolicyYaml(bytes, source);
253
+ if (source.startsWith(`${MODEL_DIR}/`)) models.push(doc);
254
+ else roles.push(doc);
255
+ parts.push({ source, bytes });
256
+ }
257
+ return { models, roles, digest: compositeDigest(parts) };
258
+ }
259
+ function compositeDigest(parts) {
260
+ const hash = createHash('sha256');
261
+ for (const part of [...parts].sort((left, right) => left.source < right.source ? -1 : left.source > right.source ? 1 : 0)) {
262
+ hash.update(part.source);
263
+ hash.update('\0');
264
+ hash.update(part.bytes);
265
+ hash.update('\0');
266
+ }
267
+ return hash.digest('hex');
268
+ }
269
+ function policyAt(commit, trusted, checkWorking) {
270
+ const loaded = readPolicySet(commit, checkWorking);
169
271
  const readBlob = (source, pinnedCommit) => {
170
272
  if (pinnedCommit === undefined) return blobAt(commit, source).bytes;
171
- // A pinned evidence commit decouples the digest from later edits to the
172
- // evidence file; the pinned commit itself must be part of trusted history.
173
273
  git('merge-base', '--is-ancestor', pinnedCommit, trusted);
174
274
  return blobAt(pinnedCommit, source).bytes;
175
275
  };
176
- return validateRosterDocument(policy, readBlob);
276
+ return { policy: validateRosterDocument(assembleRosterPolicy(loaded.models, loaded.roles), readBlob), digest: loaded.digest };
177
277
  }
178
278
  export function loadTrustedRosterPolicy() {
179
279
  const snapshot = control();
180
- const { blob, bytes } = blobAt(snapshot.head, POLICY);
181
- if (!workingBytes(POLICY).equals(bytes)) refuse('working policy differs from committed bytes');
182
- const policy = validate(bytes, snapshot.head, snapshot.trusted);
280
+ const { policy, digest } = policyAt(snapshot.head, snapshot.trusted, true);
183
281
  unchanged(snapshot);
184
- return frozen({ identity: { commit: snapshot.head, blob, sha256: sha256(bytes) }, policy });
282
+ return frozen({ identity: { commit: snapshot.head, blob: digest, sha256: digest }, policy });
185
283
  }
186
284
  export function resolveBoundPolicy(identity) {
187
285
  keys(identity, ['commit', 'blob', 'sha256'], 'identity');
188
- objectId(identity.commit, 'commit'); objectId(identity.blob, 'blob'); digest(identity.sha256);
286
+ objectId(identity.commit, 'commit'); digest(identity.blob); digest(identity.sha256);
189
287
  const snapshot = control();
190
288
  git('merge-base', '--is-ancestor', identity.commit, snapshot.trusted);
191
- const { blob, bytes } = blobAt(identity.commit, POLICY);
192
- if (blob !== identity.blob || sha256(bytes) !== identity.sha256) refuse('bound identity mismatch');
193
- const policy = validate(bytes, identity.commit, snapshot.trusted);
289
+ const loaded = policyAt(identity.commit, snapshot.trusted, false);
290
+ if (loaded.digest !== identity.blob || loaded.digest !== identity.sha256) refuse('bound identity mismatch');
194
291
  unchanged(snapshot);
195
- return frozen({ identity: { ...identity }, policy });
292
+ return frozen({ identity: { ...identity }, policy: loaded.policy });
196
293
  }