@kontextmind/kxm 0.7.128 → 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 +42 -1
- package/docs/architecture/inventory.md +1 -1
- package/docs/concepts/architecture.md +1 -1
- package/docs/concepts/data-and-storage.md +1 -1
- package/docs/contracts/README.md +1 -1
- package/docs/contracts/migration.md +12 -8
- package/docs/contracts/routing.md +3 -3
- package/docs/contributing/assignment-runner.md +14 -15
- package/docs/contributing/development.md +22 -6
- 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 +7 -7
- package/docs/operations/upgrade.md +2 -2
- package/docs/reference/cli-reference.md +37 -14
- package/docs/reference/config-reference.md +81 -155
- 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 +8 -8
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +2565 -465
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +683 -348
- package/plugins/kxm/dist/runtime.js +694 -358
- package/plugins/kxm/dist/server.js +17 -14
- 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/lanes.ts +109 -4
- package/plugins/kxm/src/cli/project.ts +6 -6
- package/plugins/kxm/src/database.ts +25 -19
- 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-service.ts +1 -0
- package/plugins/kxm/src/runtime-store.ts +390 -77
- package/plugins/kxm/src/runtime-supervisor.ts +87 -21
- 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/scripts/run-bounded.mjs +53 -0
|
@@ -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;
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
* - `.kxm/workflows/<slug>.yaml` (kxm.workflow.v1)
|
|
13
13
|
* - `.kxm/roles/<role-slug>.yaml` (kxm.role.v2)
|
|
14
14
|
* - `.kxm/models/<route-id>.yaml` (kxm.model.v2)
|
|
15
|
+
* - `plans/evidence/route-guide-qwen-pi.md` when a Pi binding matches that note
|
|
15
16
|
* - admitted selectors appended to `.kxm/routes.yaml`
|
|
16
|
-
* It never writes retired legacy authority (`.kxm/config
|
|
17
|
-
* `.kxm/roster.json`)
|
|
17
|
+
* It never writes retired legacy authority (`.kxm/config` or
|
|
18
|
+
* `.kxm/roster.json`). Role and model files are the dispatch authority.
|
|
18
19
|
*
|
|
19
20
|
* Guide research ids are not dispatch ids. Only the admitted map below is
|
|
20
21
|
* written, and only when that harness is authenticated. Unmapped ids are skipped.
|
|
@@ -28,8 +29,7 @@
|
|
|
28
29
|
* admission decision says so.
|
|
29
30
|
*/
|
|
30
31
|
|
|
31
|
-
import {
|
|
32
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
33
33
|
import { dirname, join } from "node:path";
|
|
34
34
|
import { stringify } from "yaml";
|
|
35
35
|
import { type HarnessInventory } from "./harness.ts";
|
|
@@ -446,13 +446,12 @@ function isWriterRole(roleSlug: string): boolean {
|
|
|
446
446
|
return WRITER_ROLE_PATTERN.test(roleSlug);
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
function agentDocument(roleSlug: string, stage: GuideStage
|
|
449
|
+
function agentDocument(roleSlug: string, stage: GuideStage): Record<string, unknown> {
|
|
450
450
|
const writer = isWriterRole(roleSlug);
|
|
451
451
|
return {
|
|
452
452
|
schema: "kxm.agent.v1",
|
|
453
453
|
purpose: `${stage.domain} (workflow-guide ${stage.title}; candidate verification is the operator's responsibility)`,
|
|
454
|
-
|
|
455
|
-
model: { provider: binding.provider, model: binding.model },
|
|
454
|
+
role: roleSlug,
|
|
456
455
|
tools: { preset: writer ? "workspace-writer" : "read-only" },
|
|
457
456
|
defaultRepositoryAccess: writer ? "none" : "read",
|
|
458
457
|
repositories: { control: writer ? "write" : "read" },
|
|
@@ -485,26 +484,78 @@ function workflowDocument(workflow: GuideWorkflow): Record<string, unknown> {
|
|
|
485
484
|
};
|
|
486
485
|
}
|
|
487
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Evidence written into the user's project for the one admitted guided Pi model.
|
|
489
|
+
* The hash is of these exact bytes. A Pi binding that is not this vendor, model
|
|
490
|
+
* id, and harness gets no model file and no origin.
|
|
491
|
+
*/
|
|
492
|
+
const GUIDE_PI_EVIDENCE_SOURCE = "plans/evidence/route-guide-qwen-pi.md";
|
|
493
|
+
const GUIDE_PI_EVIDENCE_SHA256 = "109f39728b251dd0565e275ae689a6e1d9b889a488d996b157d500c538115759";
|
|
494
|
+
const GUIDE_PI_NOTE = {
|
|
495
|
+
harness: "pi",
|
|
496
|
+
vendor: "openrouter",
|
|
497
|
+
model: "qwen/qwen3-coder-plus",
|
|
498
|
+
reason: "Qwen has no supported native harness on this runner, and the operator admitted that exact model for guided setup.",
|
|
499
|
+
provingCommand: "pi auth check --provider openrouter",
|
|
500
|
+
} as const;
|
|
501
|
+
|
|
502
|
+
const GUIDE_PI_EVIDENCE_NOTE = `---
|
|
503
|
+
schema: "kxm.doc.v1"
|
|
504
|
+
id: "RES-GUIDE-QWEN-PI"
|
|
505
|
+
type: "research"
|
|
506
|
+
title: "Research: guided setup Pi Qwen origin"
|
|
507
|
+
project: "kxm"
|
|
508
|
+
status: "approved"
|
|
509
|
+
owner: "@operator"
|
|
510
|
+
created: "2026-09-26"
|
|
511
|
+
updated: "2026-09-26"
|
|
512
|
+
authority: "evidence"
|
|
513
|
+
confidence: "verified"
|
|
514
|
+
summary: "Guided setup pins every Pi model origin to this note so a later project.yaml edit cannot change the hash."
|
|
515
|
+
tags: ["routing", "qwen", "pi", "init-guide"]
|
|
516
|
+
related: []
|
|
517
|
+
details:
|
|
518
|
+
research_status: "complete"
|
|
519
|
+
target_decision_date: "2026-09-26"
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
# route-guide-qwen-pi
|
|
523
|
+
|
|
524
|
+
Dated 2026-09-26. Route ids are the guided role slugs. Harness \`${GUIDE_PI_NOTE.harness}\`. Model id \`${GUIDE_PI_NOTE.model}\`. Vendor \`${GUIDE_PI_NOTE.vendor}\`. Admitted because ${GUIDE_PI_NOTE.reason} Auth was proved with \`${GUIDE_PI_NOTE.provingCommand}\`.
|
|
525
|
+
`;
|
|
526
|
+
|
|
527
|
+
function guidePiNoteMatches(binding: AgentBinding): boolean {
|
|
528
|
+
return binding.harness === GUIDE_PI_NOTE.harness
|
|
529
|
+
&& binding.provider === GUIDE_PI_NOTE.vendor
|
|
530
|
+
&& binding.model === GUIDE_PI_NOTE.model;
|
|
531
|
+
}
|
|
532
|
+
|
|
488
533
|
/**
|
|
489
534
|
* Render the planned KXM resource files (`.kxm/agents/*.yaml`,
|
|
490
|
-
* `.kxm/workflows/*.yaml
|
|
491
|
-
*
|
|
535
|
+
* `.kxm/workflows/*.yaml`, and the Pi evidence note when a binding matches it).
|
|
536
|
+
* The Pi origin is the hash of `plans/evidence/route-guide-qwen-pi.md` written
|
|
537
|
+
* into the project, whether or not `.kxm/project.yaml` exists.
|
|
492
538
|
*/
|
|
493
539
|
export function renderGuideSetupFiles(projectRoot: string, plan: GuideSetupPlan): GuideSetupFile[] {
|
|
494
|
-
const projectYaml = join(projectRoot, ".kxm", "project.yaml");
|
|
495
|
-
const projectOrigin = existsSync(projectYaml)
|
|
496
|
-
? { source: ".kxm/project.yaml", sha256: createHash("sha256").update(readFileSync(projectYaml)).digest("hex") }
|
|
497
|
-
: undefined;
|
|
498
540
|
const files: GuideSetupFile[] = [];
|
|
499
541
|
const roleStages = new Map<string, GuideStage>();
|
|
500
542
|
for (const workflow of plan.workflows) {
|
|
501
543
|
for (const stage of workflow.stages) roleStages.set(stage.role, stage);
|
|
502
544
|
}
|
|
545
|
+
let evidenceQueued = false;
|
|
503
546
|
for (const [role, binding] of [...plan.agents.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
504
547
|
const stage = roleStages.get(role);
|
|
505
548
|
if (!stage) continue;
|
|
506
549
|
const routeId = role.replaceAll("_", "-");
|
|
507
550
|
const writer = isWriterRole(role);
|
|
551
|
+
const piMatch = binding.harness === "pi" && guidePiNoteMatches(binding);
|
|
552
|
+
if (binding.harness === "pi" && !piMatch) {
|
|
553
|
+
files.push({
|
|
554
|
+
path: join(projectRoot, ".kxm", "agents", `${role}.yaml`),
|
|
555
|
+
content: stringify(agentDocument(role, stage)),
|
|
556
|
+
});
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
508
559
|
const modelDocument: Record<string, unknown> = {
|
|
509
560
|
schema: "kxm.model.v2",
|
|
510
561
|
id: routeId,
|
|
@@ -514,10 +565,19 @@ export function renderGuideSetupFiles(projectRoot: string, plan: GuideSetupPlan)
|
|
|
514
565
|
status: "admitted",
|
|
515
566
|
permissions: [writer ? "edit" : "read-only"],
|
|
516
567
|
};
|
|
517
|
-
if (
|
|
568
|
+
if (piMatch) {
|
|
569
|
+
modelDocument.origin = { source: GUIDE_PI_EVIDENCE_SOURCE, sha256: GUIDE_PI_EVIDENCE_SHA256 };
|
|
570
|
+
if (!evidenceQueued) {
|
|
571
|
+
files.push({
|
|
572
|
+
path: join(projectRoot, GUIDE_PI_EVIDENCE_SOURCE),
|
|
573
|
+
content: GUIDE_PI_EVIDENCE_NOTE,
|
|
574
|
+
});
|
|
575
|
+
evidenceQueued = true;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
518
578
|
files.push({
|
|
519
579
|
path: join(projectRoot, ".kxm", "agents", `${role}.yaml`),
|
|
520
|
-
content: stringify(agentDocument(role, stage
|
|
580
|
+
content: stringify(agentDocument(role, stage)),
|
|
521
581
|
});
|
|
522
582
|
files.push({
|
|
523
583
|
path: join(projectRoot, ".kxm", "models", `${routeId}.yaml`),
|
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.131";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|