@kontextmind/kxm 0.7.130 → 0.7.131
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/README.md +4 -1
- package/.kxm/agents/coordinator.yaml +1 -0
- package/.kxm/agents/critic-arch.yaml +1 -4
- package/.kxm/agents/critic-cli.yaml +1 -4
- package/.kxm/agents/implementer.yaml +1 -4
- package/.kxm/workflows/implement-only.yaml +1 -1
- package/.kxm/workflows/review-arch-only.yaml +1 -1
- package/.kxm/workflows/review-cli-only.yaml +1 -1
- package/CHANGELOG.md +22 -1
- package/docs/concepts/architecture.md +1 -1
- package/docs/contracts/routing.md +3 -3
- package/docs/contributing/assignment-runner.md +14 -15
- package/docs/contributing/development.md +19 -5
- package/docs/contributing/harness-routing-internals.md +8 -8
- package/docs/glossary.md +1 -1
- package/docs/guides/agent-skills.md +1 -1
- package/docs/operations/backup-and-restore.md +4 -4
- package/docs/reference/cli-reference.md +26 -8
- package/docs/reference/config-reference.md +76 -153
- package/docs/reference/harness-routing.md +29 -21
- package/docs/reference/workflow-catalog.md +4 -4
- package/docs/start/first-workflow.md +1 -1
- package/examples/project/.kxm/agents/coordinator.yaml +1 -2
- package/examples/project/.kxm/agents/critic-1.yaml +1 -3
- package/examples/project/.kxm/agents/critic-2.yaml +1 -2
- package/examples/project/.kxm/agents/critic-3.yaml +2 -3
- package/examples/project/.kxm/agents/implementer.yaml +1 -2
- package/examples/project/.kxm/agents/planner.yaml +1 -2
- package/examples/project/.kxm/agents/reproducer.yaml +1 -2
- package/examples/project/.kxm/agents/reviewer.yaml +1 -2
- package/examples/project/.kxm/workflows/fix.yaml +0 -12
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +287 -249
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +222 -242
- package/plugins/kxm/dist/runtime.js +224 -244
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +8 -0
- package/plugins/kxm/src/cli/project.ts +4 -4
- package/plugins/kxm/src/engine.ts +168 -163
- package/plugins/kxm/src/init-guide-setup.ts +75 -15
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/oneshot-producer.ts +3 -15
- package/plugins/kxm/src/project-config.ts +83 -70
- package/plugins/kxm/src/routes.ts +26 -18
- package/plugins/kxm/src/runtime-supervisor.ts +0 -18
- package/plugins/kxm/src/suggest.ts +2 -2
- package/plugins/kxm/src/template.ts +2 -14
- package/schemas/agent.schema.json +8 -0
- package/scripts/roster-policy.d.mts +2 -1
- 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
|
|
12
|
-
const
|
|
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
|
-
|
|
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
|
|
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, ['
|
|
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
|
|
166
|
-
let
|
|
167
|
-
try {
|
|
168
|
-
if (
|
|
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(
|
|
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 {
|
|
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:
|
|
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');
|
|
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
|
|
192
|
-
if (
|
|
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
|
}
|