@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.
- 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
package/plugins/kxm/package.json
CHANGED
|
@@ -7,7 +7,7 @@ description: Entry point for KXM, the kxm CLI and the Claude Code plugin kxm_* M
|
|
|
7
7
|
|
|
8
8
|
Pick the skill that owns the request, then follow it. These bundled skills
|
|
9
9
|
document the current CLI. They do not switch runtime YAML authority, admit
|
|
10
|
-
writers, or replace
|
|
10
|
+
writers, or replace the trusted role and model policy.
|
|
11
11
|
|
|
12
12
|
## Route by request
|
|
13
13
|
|
|
@@ -10,6 +10,14 @@ references skills, constrains tools, and declares harness/model choices.
|
|
|
10
10
|
A model appearing in a roster is not proof of authentication, capability,
|
|
11
11
|
or permission to execute an assignment.
|
|
12
12
|
|
|
13
|
+
Dispatch reads `.kxm/roles/*.yaml` and `.kxm/models/*.yaml`. An agent binds
|
|
14
|
+
a role with `role:` and does not pin `harness` or `model`. An agent
|
|
15
|
+
`tools.preset` must be a registered preset. Narrowing that preset against
|
|
16
|
+
the role preset is not validated yet (P3). `kxm routes` prints
|
|
17
|
+
admission from `.kxm/routes.yaml` and membership from the role files.
|
|
18
|
+
The developer runner builds its policy object from those same files at
|
|
19
|
+
`refs/remotes/origin/main`.
|
|
20
|
+
|
|
13
21
|
## Inspect Before Editing
|
|
14
22
|
|
|
15
23
|
```bash
|
|
@@ -6,7 +6,7 @@ import { createInterface } from "node:readline";
|
|
|
6
6
|
import { applyRestorePlan, createBackup, databaseError, planBackup, planRestore, type RestorePlan } from "../database.ts";
|
|
7
7
|
import { readLiveHubClaim } from "../hub-autostart.ts";
|
|
8
8
|
import { refreshModelInventory } from "../model-inventory.ts";
|
|
9
|
-
import { listInventoryModels, listRoleBindings, loadRoutePolicy, setRouteState, updateRouteState } from "../routes.ts";
|
|
9
|
+
import { listInventoryModels, listRoleBindings, loadRoutePolicy, rolesForInventoryModel, setRouteState, updateRouteState } from "../routes.ts";
|
|
10
10
|
import {
|
|
11
11
|
KxmConfigError,
|
|
12
12
|
discoverKxmProjectRoot,
|
|
@@ -889,8 +889,7 @@ export async function cmdModelsScreen(runtime: Runtime): Promise<number> {
|
|
|
889
889
|
try {
|
|
890
890
|
while (true) {
|
|
891
891
|
const policy = loadRoutePolicy(runtime.dirs.workdir);
|
|
892
|
-
|
|
893
|
-
runtime.io.stdout(models.map((m, i) => `${i + 1}. ${m} [${policy.admitted.includes(m) ? "admitted" : policy.disabled.includes(m) ? "disabled" : "unset"}] roles:${Object.entries(roleBindings).filter(([, xs]) => xs.includes(m)).map(([r]) => r).join(",") || "-"}`).join("\n") + "\n");
|
|
892
|
+
runtime.io.stdout(models.map((m, i) => `${i + 1}. ${m} [${policy.admitted.includes(m) ? "admitted" : policy.disabled.includes(m) ? "disabled" : "unset"}] roles:${rolesForInventoryModel(runtime.dirs.workdir, m).join(",") || "-"}`).join("\n") + "\n");
|
|
894
893
|
const command = (await ask("[a]dmit [d]isable [r]ole-add [x]ole-remove [q]uit: ")).trim().toLowerCase();
|
|
895
894
|
if (command === "q" || command === "quit") return 0;
|
|
896
895
|
const index = Number.parseInt((await ask("model number: ")).trim(), 10) - 1;
|
|
@@ -910,7 +909,8 @@ export async function cmdModelsScreen(runtime: Runtime): Promise<number> {
|
|
|
910
909
|
|
|
911
910
|
export async function cmdRouteList(runtime: Runtime): Promise<number> {
|
|
912
911
|
const policy = loadRoutePolicy(runtime.dirs.workdir);
|
|
913
|
-
|
|
912
|
+
const membership = Object.entries(listRoleBindings(runtime.dirs.workdir)).flatMap(([role, ids]) => ids.map((id) => `${role} ${id}`));
|
|
913
|
+
print(runtime.io, runtime.json, { ok: true, command: "routes list", policy, membership }, [...policy.admitted.map((x) => `admitted ${x}`), ...policy.disabled.map((x) => `disabled ${x}`), ...membership].join("\n") || "no route decisions");
|
|
914
914
|
return 0;
|
|
915
915
|
}
|
|
916
916
|
|
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
|
-
import {
|
|
5
|
+
import { listRoleBindings, loadRoutePolicy } from "./routes.ts";
|
|
6
6
|
import { applyAuthoringWitness, captureWorktreeWitness } from "./worktree-witness.ts";
|
|
7
7
|
import {
|
|
8
8
|
buildFormalContextPacket,
|
|
@@ -1342,26 +1342,30 @@ export interface KxmPanelDispatchSeams {
|
|
|
1342
1342
|
|
|
1343
1343
|
export const kxmPanelDispatchSeams: KxmPanelDispatchSeams = {};
|
|
1344
1344
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1345
|
+
interface ResolvedProducerRoute {
|
|
1346
|
+
provider: string;
|
|
1347
|
+
model: string;
|
|
1348
|
+
selector: string;
|
|
1349
|
+
harness: string;
|
|
1350
|
+
routeId: string;
|
|
1351
|
+
role: string;
|
|
1352
|
+
permissions: string[];
|
|
1353
|
+
effort?: string | undefined;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function readYamlFile(file: string): Record<string, unknown> | undefined {
|
|
1357
|
+
return readModelDocument(file);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function readModelDocument(file: string): Record<string, unknown> | undefined {
|
|
1361
|
+
if (!existsSync(file)) return undefined;
|
|
1362
|
+
try {
|
|
1363
|
+
const parsed = parse(readFileSync(file, "utf8")) as unknown;
|
|
1364
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
1365
|
+
return parsed as Record<string, unknown>;
|
|
1366
|
+
} catch {
|
|
1367
|
+
return undefined;
|
|
1363
1368
|
}
|
|
1364
|
-
return [...selectors];
|
|
1365
1369
|
}
|
|
1366
1370
|
|
|
1367
1371
|
function unreconciledPanelAttemptId(state: KxmRunState): string | undefined {
|
|
@@ -1383,97 +1387,119 @@ function resolveProducerRoute(
|
|
|
1383
1387
|
projectRoot: string,
|
|
1384
1388
|
step: KxmCompiledStep,
|
|
1385
1389
|
agentId: string,
|
|
1386
|
-
):
|
|
1387
|
-
|
|
1388
|
-
const agentFile = join(projectRoot, ".kxm", "agents", `${agentId}.yaml`);
|
|
1389
|
-
if (existsSync(agentFile)) {
|
|
1390
|
-
try {
|
|
1391
|
-
const parsed = parse(readFileSync(agentFile, "utf8")) as Record<string, unknown>;
|
|
1392
|
-
if (typeof parsed?.model === "string") {
|
|
1393
|
-
agentModel = parsed.model;
|
|
1394
|
-
} else if (parsed?.model && typeof parsed.model === "object" && !Array.isArray(parsed.model)) {
|
|
1395
|
-
// The agent schema requires the object form (`{provider, model}`); reading only the
|
|
1396
|
-
// string form left every agent without a route unless it was literally named
|
|
1397
|
-
// `implementer`, which alone hit the hard-coded fallback below. A declared model
|
|
1398
|
-
// must actually drive the producer route. Model ids may themselves be namespaced
|
|
1399
|
-
// (`qwen/qwen3-coder-plus` under `openrouter`) — the provider is the only field
|
|
1400
|
-
// that may not contain the separator, because it is what the selector splits on.
|
|
1401
|
-
// A malformed declaration is an error, not an absence: falling through to the
|
|
1402
|
-
// fallback would silently route an agent that declared `openrouter` to Grok.
|
|
1403
|
-
const declared = parsed.model as { provider?: unknown; model?: unknown };
|
|
1404
|
-
const providerOk = typeof declared.provider === "string" && declared.provider.length > 0 && !declared.provider.includes("/");
|
|
1405
|
-
const modelOk = typeof declared.model === "string" && declared.model.length > 0 && !declared.model.startsWith("/") && !declared.model.endsWith("/");
|
|
1406
|
-
if (providerOk && modelOk) {
|
|
1407
|
-
agentModel = `${declared.provider}/${declared.model}`;
|
|
1408
|
-
} else {
|
|
1409
|
-
return {
|
|
1410
|
-
error: {
|
|
1411
|
-
reason: "step_unsupported",
|
|
1412
|
-
field: "model",
|
|
1413
|
-
detail: "producer_route_unsupported: invalid model declaration",
|
|
1414
|
-
},
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
} catch {
|
|
1419
|
-
// ignore
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
let selector = typeof step.model === "string" ? step.model : agentModel;
|
|
1424
|
-
if (!selector) {
|
|
1425
|
-
if (agentId === "implementer") {
|
|
1426
|
-
selector = "xai/grok-4.6";
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
if (!selector) {
|
|
1390
|
+
): ResolvedProducerRoute | { error: Omit<KxmRunHandoff, "stepId"> } {
|
|
1391
|
+
if ((step.kind === "agent" || step.kind === "moa") && step.model !== undefined) {
|
|
1430
1392
|
return {
|
|
1431
1393
|
error: {
|
|
1432
1394
|
reason: "step_unsupported",
|
|
1433
1395
|
field: "model",
|
|
1434
|
-
detail: "producer_route_unsupported: agent
|
|
1396
|
+
detail: "producer_route_unsupported: agent step model is not honored",
|
|
1435
1397
|
},
|
|
1436
1398
|
};
|
|
1437
1399
|
}
|
|
1438
|
-
|
|
1439
|
-
const
|
|
1440
|
-
if (
|
|
1400
|
+
const agent = readYamlFile(join(projectRoot, ".kxm", "agents", `${agentId}.yaml`));
|
|
1401
|
+
const role = typeof agent?.role === "string" ? agent.role : "";
|
|
1402
|
+
if (!role) {
|
|
1441
1403
|
return {
|
|
1442
1404
|
error: {
|
|
1443
1405
|
reason: "step_unsupported",
|
|
1444
1406
|
field: "model",
|
|
1445
|
-
detail:
|
|
1407
|
+
detail: "producer_route_unsupported: agent has no role",
|
|
1446
1408
|
},
|
|
1447
1409
|
};
|
|
1448
1410
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1411
|
+
const roleDoc = readYamlFile(join(projectRoot, ".kxm", "roles", `${role}.yaml`));
|
|
1412
|
+
const roster = Array.isArray(roleDoc?.roster) ? roleDoc.roster : undefined;
|
|
1413
|
+
if (!roster) {
|
|
1451
1414
|
return {
|
|
1452
1415
|
error: {
|
|
1453
1416
|
reason: "step_unsupported",
|
|
1454
1417
|
field: "model",
|
|
1455
|
-
detail: `producer_route_unsupported:
|
|
1418
|
+
detail: `producer_route_unsupported: role '${role}' has no roster`,
|
|
1456
1419
|
},
|
|
1457
1420
|
};
|
|
1458
1421
|
}
|
|
1459
|
-
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1422
|
+
const policy = loadRoutePolicy(projectRoot);
|
|
1423
|
+
for (const entry of roster) {
|
|
1424
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
1425
|
+
const record = entry as { route?: unknown; effort?: unknown };
|
|
1426
|
+
if (typeof record.route !== "string" || record.route.length === 0) continue;
|
|
1427
|
+
const modelPath = join(projectRoot, ".kxm", "models", `${record.route}.yaml`);
|
|
1428
|
+
const doc = readModelDocument(modelPath);
|
|
1429
|
+
if (!doc) {
|
|
1430
|
+
return {
|
|
1431
|
+
error: {
|
|
1432
|
+
reason: "step_unsupported",
|
|
1433
|
+
field: "model",
|
|
1434
|
+
detail: `producer_route_unsupported: model file '.kxm/models/${record.route}.yaml' is missing or unreadable`,
|
|
1435
|
+
},
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
if (doc.status !== "admitted") continue;
|
|
1439
|
+
const modelName = typeof doc.model === "string" ? doc.model : "";
|
|
1440
|
+
const vendor = typeof doc.vendor === "string" ? doc.vendor : "";
|
|
1441
|
+
const harness = typeof doc.harness === "string" ? doc.harness : "";
|
|
1442
|
+
if (!modelName || !vendor || !harness) continue;
|
|
1443
|
+
const candidates = [modelName, `${vendor}/${modelName}`, `${harness}/${modelName}`];
|
|
1444
|
+
const admittedSelector = candidates.find((candidate) => policy.admitted.includes(candidate));
|
|
1445
|
+
const disabledSelector = candidates.find((candidate) => policy.disabled.includes(candidate));
|
|
1446
|
+
if (!admittedSelector && disabledSelector) {
|
|
1447
|
+
return {
|
|
1448
|
+
error: {
|
|
1449
|
+
reason: "step_unsupported",
|
|
1450
|
+
field: "model",
|
|
1451
|
+
detail: `producer_route_unsupported: first admitted route '${record.route}' for role '${role}' is disabled`,
|
|
1452
|
+
},
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const selector = admittedSelector ?? (modelName.includes("/") ? modelName : `${vendor}/${modelName}`);
|
|
1456
|
+
if (policy.disabled.includes(selector)) {
|
|
1457
|
+
return {
|
|
1458
|
+
error: {
|
|
1459
|
+
reason: "step_unsupported",
|
|
1460
|
+
field: "model",
|
|
1461
|
+
detail: `producer_route_unsupported: first admitted route '${record.route}' for role '${role}' is disabled`,
|
|
1462
|
+
},
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
if (!policy.admitted.includes(selector)) {
|
|
1466
|
+
return {
|
|
1467
|
+
error: {
|
|
1468
|
+
reason: "step_unsupported",
|
|
1469
|
+
field: "model",
|
|
1470
|
+
detail: `producer_route_unsupported: model '${selector}' is not admitted`,
|
|
1471
|
+
},
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
const slash = selector.indexOf("/");
|
|
1475
|
+
if (slash <= 0 || slash === selector.length - 1) {
|
|
1476
|
+
return {
|
|
1477
|
+
error: {
|
|
1478
|
+
reason: "step_unsupported",
|
|
1479
|
+
field: "model",
|
|
1480
|
+
detail: `producer_route_unsupported: invalid selector '${selector}'`,
|
|
1481
|
+
},
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
const permissions = Array.isArray(doc.permissions) ? doc.permissions.filter((item): item is string => typeof item === "string") : [];
|
|
1465
1485
|
return {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1486
|
+
provider: selector.slice(0, slash),
|
|
1487
|
+
model: selector.slice(slash + 1),
|
|
1488
|
+
selector,
|
|
1489
|
+
harness,
|
|
1490
|
+
routeId: record.route,
|
|
1491
|
+
role,
|
|
1492
|
+
permissions,
|
|
1493
|
+
...(typeof record.effort === "string" ? { effort: record.effort } : {}),
|
|
1471
1494
|
};
|
|
1472
1495
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1496
|
+
return {
|
|
1497
|
+
error: {
|
|
1498
|
+
reason: "step_unsupported",
|
|
1499
|
+
field: "model",
|
|
1500
|
+
detail: `producer_route_unsupported: role '${role}' has no admitted route`,
|
|
1501
|
+
},
|
|
1502
|
+
};
|
|
1477
1503
|
}
|
|
1478
1504
|
|
|
1479
1505
|
function projectAgentStepTimeoutMs(context: KxmRuntimeContext, run: KxmRunRecord): number {
|
|
@@ -1708,7 +1734,7 @@ function prepareDispatch(
|
|
|
1708
1734
|
const wideTimeout = unsupportedAgentStepTimeout(step, projectAgentStepTimeoutMs(context, run));
|
|
1709
1735
|
if (wideTimeout) return { kind: "return", state, handoff: { ...wideTimeout, stepId } };
|
|
1710
1736
|
|
|
1711
|
-
let resolvedRoute:
|
|
1737
|
+
let resolvedRoute: ResolvedProducerRoute | undefined;
|
|
1712
1738
|
if (producerId !== "driver-simulated") {
|
|
1713
1739
|
const allowed = step.assignments.allowedAgents;
|
|
1714
1740
|
const agentId = (allowed && allowed.length > 0 && allowed[0])
|
|
@@ -1722,8 +1748,7 @@ function prepareDispatch(
|
|
|
1722
1748
|
const writeRefusal = unsupportedLiveWrite(
|
|
1723
1749
|
context.projectRoot,
|
|
1724
1750
|
step,
|
|
1725
|
-
|
|
1726
|
-
resolvedRoute.selector,
|
|
1751
|
+
resolvedRoute,
|
|
1727
1752
|
loadKxmRunPlanEnvelope(context.eventStore, run).projectLimits.maxConcurrentRuns,
|
|
1728
1753
|
);
|
|
1729
1754
|
if (writeRefusal) return { kind: "return", state, handoff: { ...writeRefusal, stepId } };
|
|
@@ -1753,7 +1778,7 @@ function prepareDispatch(
|
|
|
1753
1778
|
for (const event of events) context.eventStore.appendEvent(event);
|
|
1754
1779
|
const entered = foldStoredKxmRun(context, run);
|
|
1755
1780
|
persistKxmRunState(context, runId, entered, events[events.length - 1]!.sequence);
|
|
1756
|
-
const
|
|
1781
|
+
const firstBirth = birthMember(context, {
|
|
1757
1782
|
run,
|
|
1758
1783
|
plan,
|
|
1759
1784
|
step,
|
|
@@ -1764,6 +1789,10 @@ function prepareDispatch(
|
|
|
1764
1789
|
resolvedRoute,
|
|
1765
1790
|
dispatchSources,
|
|
1766
1791
|
});
|
|
1792
|
+
if ("handoff" in firstBirth) {
|
|
1793
|
+
return { kind: "return", state: entered, handoff: firstBirth.handoff };
|
|
1794
|
+
}
|
|
1795
|
+
const first = firstBirth;
|
|
1767
1796
|
return {
|
|
1768
1797
|
kind: "panel",
|
|
1769
1798
|
panel: {
|
|
@@ -1822,10 +1851,10 @@ function birthMember(
|
|
|
1822
1851
|
stepAttempt: number;
|
|
1823
1852
|
enterRunning?: boolean | undefined;
|
|
1824
1853
|
producerId?: ("driver-simulated" | "pi" | string) | undefined;
|
|
1825
|
-
resolvedRoute?:
|
|
1854
|
+
resolvedRoute?: ResolvedProducerRoute | undefined;
|
|
1826
1855
|
dispatchSources: DispatchContextSources;
|
|
1827
1856
|
},
|
|
1828
|
-
): PreparedDispatch {
|
|
1857
|
+
): PreparedDispatch | { handoff: KxmRunHandoff } {
|
|
1829
1858
|
const run = requireRun(context, input.run.runId);
|
|
1830
1859
|
const folded = foldStoredKxmRun(context, run);
|
|
1831
1860
|
if (!birthAllowed(folded, input.step)) {
|
|
@@ -1848,9 +1877,10 @@ function birthMember(
|
|
|
1848
1877
|
let resolvedRoute = input.resolvedRoute;
|
|
1849
1878
|
if (!resolvedRoute && input.producerId && input.producerId !== "driver-simulated") {
|
|
1850
1879
|
const routeResult = resolveProducerRoute(context.projectRoot, input.step, agentId);
|
|
1851
|
-
if (
|
|
1852
|
-
|
|
1880
|
+
if ("error" in routeResult) {
|
|
1881
|
+
return { handoff: { ...routeResult.error, stepId: input.stepId } };
|
|
1853
1882
|
}
|
|
1883
|
+
resolvedRoute = routeResult;
|
|
1854
1884
|
}
|
|
1855
1885
|
const dispatchContext = assembleDispatchContext(input.dispatchSources, {
|
|
1856
1886
|
projectId: run.projectId,
|
|
@@ -1958,13 +1988,13 @@ function birthMember(
|
|
|
1958
1988
|
allowedOutcomes: input.step.outcomes,
|
|
1959
1989
|
signal: controller.signal,
|
|
1960
1990
|
prompt: input.step.instructions ? `${input.step.instructions}\n\n${generatedPrompt}` : generatedPrompt,
|
|
1961
|
-
thinking: input.stepAttempt <= 1 ? "low" : "medium",
|
|
1962
1991
|
permission: Object.values(input.step.repositories).some((access) => access === "write") ? "edit" : "read-only",
|
|
1963
1992
|
...((input.step.kind === "agent" || input.step.kind === "moa") && input.step.timeoutMs !== undefined
|
|
1964
1993
|
? { timeoutMs: input.step.timeoutMs }
|
|
1965
1994
|
: {}),
|
|
1966
1995
|
contextPacket,
|
|
1967
|
-
...(resolvedRoute ? {
|
|
1996
|
+
...(resolvedRoute?.effort !== undefined ? { thinking: resolvedRoute.effort } : {}),
|
|
1997
|
+
...(resolvedRoute ? { harness: resolvedRoute.harness, provider: resolvedRoute.provider, model: resolvedRoute.model } : {}),
|
|
1968
1998
|
},
|
|
1969
1999
|
controller,
|
|
1970
2000
|
state: next,
|
|
@@ -2157,13 +2187,14 @@ async function drivePanel(
|
|
|
2157
2187
|
pending.set(member.attemptId, work);
|
|
2158
2188
|
};
|
|
2159
2189
|
|
|
2190
|
+
let routeHandoff: KxmRunHandoff | undefined;
|
|
2160
2191
|
const tryBirth = (): PreparedDispatch | undefined => {
|
|
2161
|
-
if (stopBirths) return undefined;
|
|
2192
|
+
if (stopBirths || routeHandoff) return undefined;
|
|
2162
2193
|
return context.eventStore.transaction(() => {
|
|
2163
2194
|
const run = requireRun(context, runId);
|
|
2164
2195
|
const state = foldStoredKxmRun(context, run);
|
|
2165
2196
|
if (!birthAllowed(state, panel.step)) return undefined;
|
|
2166
|
-
|
|
2197
|
+
const born = birthMember(context, {
|
|
2167
2198
|
run,
|
|
2168
2199
|
plan: panel.plan,
|
|
2169
2200
|
step: panel.step,
|
|
@@ -2172,6 +2203,11 @@ async function drivePanel(
|
|
|
2172
2203
|
producerId: producer.id,
|
|
2173
2204
|
dispatchSources: panel.dispatchSources,
|
|
2174
2205
|
});
|
|
2206
|
+
if ("handoff" in born) {
|
|
2207
|
+
routeHandoff = born.handoff;
|
|
2208
|
+
return undefined;
|
|
2209
|
+
}
|
|
2210
|
+
return born;
|
|
2175
2211
|
});
|
|
2176
2212
|
};
|
|
2177
2213
|
|
|
@@ -2218,6 +2254,10 @@ async function drivePanel(
|
|
|
2218
2254
|
while (!stopBirths && !settlementFailed) {
|
|
2219
2255
|
try {
|
|
2220
2256
|
const next = tryBirth();
|
|
2257
|
+
if (routeHandoff) {
|
|
2258
|
+
stopBirths = true;
|
|
2259
|
+
break;
|
|
2260
|
+
}
|
|
2221
2261
|
if (!next) break;
|
|
2222
2262
|
launch(next);
|
|
2223
2263
|
} catch {
|
|
@@ -2228,6 +2268,13 @@ async function drivePanel(
|
|
|
2228
2268
|
return unreconciledHandoff(state, panel.stepId);
|
|
2229
2269
|
}
|
|
2230
2270
|
}
|
|
2271
|
+
if (routeHandoff) {
|
|
2272
|
+
const handoff = routeHandoff;
|
|
2273
|
+
abortOwned();
|
|
2274
|
+
await drainPendingInvoked();
|
|
2275
|
+
const state = foldStoredKxmRun(context, requireRun(context, runId));
|
|
2276
|
+
return { state, handoff };
|
|
2277
|
+
}
|
|
2231
2278
|
if (pending.size === 0) break;
|
|
2232
2279
|
const finished = await Promise.race(pending.values());
|
|
2233
2280
|
pending.delete(finished.attemptId);
|
|
@@ -2852,26 +2899,28 @@ export function kxmLiveRunPrerequisites(
|
|
|
2852
2899
|
if (step.kind !== "agent" && step.kind !== "moa") continue;
|
|
2853
2900
|
const agentIds = step.assignments.allowedAgents.length > 0 ? step.assignments.allowedAgents : [step.agent];
|
|
2854
2901
|
for (const agentId of agentIds) {
|
|
2855
|
-
const
|
|
2856
|
-
|
|
2902
|
+
const route = resolveProducerRoute(projectRoot, step, agentId);
|
|
2903
|
+
if ("error" in route) {
|
|
2904
|
+
const detail = route.error.detail === "producer_route_unsupported: agent step model is not honored"
|
|
2905
|
+
? `${agentId}: agent step model is not honored; remove model from the step`
|
|
2906
|
+
: `${agentId}: ${route.error.detail}; set role in .kxm/agents/${agentId}.yaml and admit the route model with kxm routes admit --model <provider/model>`;
|
|
2907
|
+
prerequisites.push({ ...route.error, stepId, detail });
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
const harness = route.harness;
|
|
2857
2911
|
const permission = Object.values(step.repositories).includes("write") ? "edit" : "read-only";
|
|
2858
2912
|
if (!BUILTIN_HARNESSES.some((entry) => entry.id === harness && entry.oneShot) || !oneShotPermissionArgs(harness, permission)) {
|
|
2859
2913
|
prerequisites.push({ reason: "step_unsupported", stepId, field: "harness", detail: `${agentId}: ${harness} has no audited ${permission} one-shot profile; use a supported workflow or execute this work directly in ${harness}, without substituting another harness` });
|
|
2860
2914
|
continue;
|
|
2861
2915
|
}
|
|
2862
|
-
const
|
|
2863
|
-
if ("error" in route) {
|
|
2864
|
-
prerequisites.push({ ...route.error, stepId, detail: `${agentId}: ${route.error.detail}; configure its model in .kxm/agents/${agentId}.yaml and admit the installed model with kxm routes admit --model <provider/model>` });
|
|
2865
|
-
continue;
|
|
2866
|
-
}
|
|
2867
|
-
const writeRefusal = unsupportedLiveWrite(projectRoot, step, agentId, route.selector, envelope.projectLimits.maxConcurrentRuns);
|
|
2916
|
+
const writeRefusal = unsupportedLiveWrite(projectRoot, step, route, envelope.projectLimits.maxConcurrentRuns);
|
|
2868
2917
|
if (writeRefusal) {
|
|
2869
2918
|
prerequisites.push({ ...writeRefusal, stepId });
|
|
2870
2919
|
continue;
|
|
2871
2920
|
}
|
|
2872
2921
|
const compatible = validateHarnessModelPair(harness, route);
|
|
2873
2922
|
if (!compatible.valid) {
|
|
2874
|
-
prerequisites.push({ reason: "step_unsupported", stepId, field: "model", detail: `${agentId}: ${compatible.message ?? `${harness} cannot run ${route.selector}`}; configure a model supported by ${harness}
|
|
2923
|
+
prerequisites.push({ reason: "step_unsupported", stepId, field: "model", detail: `${agentId}: ${compatible.message ?? `${harness} cannot run ${route.selector}`}; configure a model supported by ${harness} on the role roster` });
|
|
2875
2924
|
}
|
|
2876
2925
|
}
|
|
2877
2926
|
}
|
|
@@ -2951,31 +3000,9 @@ function unsupportedStep(
|
|
|
2951
3000
|
return undefined;
|
|
2952
3001
|
}
|
|
2953
3002
|
|
|
2954
|
-
function readYamlRecord(path: string): Record<string, unknown> | undefined {
|
|
2955
|
-
if (!existsSync(path)) return undefined;
|
|
2956
|
-
try {
|
|
2957
|
-
const parsed = parse(readFileSync(path, "utf8")) as unknown;
|
|
2958
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>;
|
|
2959
|
-
} catch {
|
|
2960
|
-
return undefined;
|
|
2961
|
-
}
|
|
2962
|
-
return undefined;
|
|
2963
|
-
}
|
|
2964
|
-
|
|
2965
|
-
function agentHarness(projectRoot: string, agentId: string): string | undefined {
|
|
2966
|
-
const harness = readYamlRecord(join(projectRoot, ".kxm", "agents", `${agentId}.yaml`))?.harness;
|
|
2967
|
-
return typeof harness === "string" && harness.length > 0 ? harness : undefined;
|
|
2968
|
-
}
|
|
2969
|
-
|
|
2970
|
-
function projectDefaultHarness(projectRoot: string): string {
|
|
2971
|
-
const harness = readYamlRecord(join(projectRoot, ".kxm", "project.yaml"))?.defaultHarness;
|
|
2972
|
-
return typeof harness === "string" && harness.length > 0 ? harness : "pi";
|
|
2973
|
-
}
|
|
2974
|
-
|
|
2975
3003
|
/**
|
|
2976
3004
|
* Live write steps run only on an audited writer profile, and only when the
|
|
2977
|
-
*
|
|
2978
|
-
* A missing roster is a fresh project: route admission is the other gate.
|
|
3005
|
+
* writer roster names that route as admitted with edit.
|
|
2979
3006
|
*
|
|
2980
3007
|
* The authoring witness fingerprints the one project checkout around each
|
|
2981
3008
|
* spawn, so it can only attribute a change to a lone writer. A write step with
|
|
@@ -2987,8 +3014,7 @@ function projectDefaultHarness(projectRoot: string): string {
|
|
|
2987
3014
|
function unsupportedLiveWrite(
|
|
2988
3015
|
projectRoot: string,
|
|
2989
3016
|
step: KxmCompiledStep,
|
|
2990
|
-
|
|
2991
|
-
selector: string,
|
|
3017
|
+
route: ResolvedProducerRoute,
|
|
2992
3018
|
maxConcurrentRuns: number,
|
|
2993
3019
|
): Omit<KxmRunHandoff, "stepId"> | undefined {
|
|
2994
3020
|
if (!Object.values(step.repositories).some((access) => access === "write")) return undefined;
|
|
@@ -3006,44 +3032,23 @@ function unsupportedLiveWrite(
|
|
|
3006
3032
|
detail: "live write steps require limits.maxConcurrentRuns of 1; concurrent runs share one checkout",
|
|
3007
3033
|
};
|
|
3008
3034
|
}
|
|
3009
|
-
|
|
3010
|
-
if (!oneShotWriterArgs(harness)) {
|
|
3035
|
+
if (!oneShotWriterArgs(route.harness)) {
|
|
3011
3036
|
return {
|
|
3012
3037
|
reason: "step_unsupported",
|
|
3013
3038
|
field: "repositories",
|
|
3014
|
-
detail: `live write steps require an audited writer profile; ${harness} has none`,
|
|
3039
|
+
detail: `live write steps require an audited writer profile; ${route.harness} has none`,
|
|
3015
3040
|
};
|
|
3016
3041
|
}
|
|
3017
|
-
const
|
|
3018
|
-
if (!
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
const routes = roster.routes;
|
|
3024
|
-
const lineup = roster.lineup;
|
|
3025
|
-
const writerIds = lineup && typeof lineup === "object" && !Array.isArray(lineup)
|
|
3026
|
-
? (lineup as Record<string, unknown>).writer
|
|
3027
|
-
: undefined;
|
|
3028
|
-
if (!routes || typeof routes !== "object" || Array.isArray(routes) || !Array.isArray(writerIds)) {
|
|
3029
|
-
return { reason: "step_unsupported", field: "model", detail: "developer roster has no writer lineup" };
|
|
3030
|
-
}
|
|
3031
|
-
const allowed = writerIds.some((id) => {
|
|
3032
|
-
if (typeof id !== "string") return false;
|
|
3033
|
-
const route = (routes as Record<string, unknown>)[id];
|
|
3034
|
-
if (!route || typeof route !== "object" || Array.isArray(route)) return false;
|
|
3035
|
-
const record = route as Record<string, unknown>;
|
|
3036
|
-
if (record.harness !== harness || record.status !== "admitted") return false;
|
|
3037
|
-
if (!Array.isArray(record.permissions) || !record.permissions.includes("edit")) return false;
|
|
3038
|
-
const model = typeof record.model === "string" ? record.model : "";
|
|
3039
|
-
const vendor = typeof record.vendor === "string" ? record.vendor : "";
|
|
3040
|
-
return model === selector || (vendor.length > 0 && `${vendor}/${model}` === selector);
|
|
3041
|
-
});
|
|
3042
|
-
if (!allowed) {
|
|
3042
|
+
const writerIds = listRoleBindings(projectRoot).writer;
|
|
3043
|
+
if (!Array.isArray(writerIds) || writerIds.length === 0) {
|
|
3044
|
+
return { reason: "step_unsupported", field: "model", detail: "writer roster has no admitted edit route" };
|
|
3045
|
+
}
|
|
3046
|
+
const named = writerIds.includes(route.routeId);
|
|
3047
|
+
if (!named || !route.permissions.includes("edit")) {
|
|
3043
3048
|
return {
|
|
3044
3049
|
reason: "step_unsupported",
|
|
3045
3050
|
field: "model",
|
|
3046
|
-
detail: `live write route ${selector} on ${harness} is not on the
|
|
3051
|
+
detail: `live write route ${route.selector} on ${route.harness} is not an admitted edit route on the writer roster`,
|
|
3047
3052
|
};
|
|
3048
3053
|
}
|
|
3049
3054
|
return undefined;
|