@kontextmind/kxm 0.7.144 → 0.7.146
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 +20 -2
- package/.kxm/agents/{coordinator.yaml → planner.yaml} +2 -0
- package/.kxm/agents/{critic-arch.yaml → reviewer-arch.yaml} +2 -0
- package/.kxm/agents/{critic-cli.yaml → reviewer-cli.yaml} +2 -0
- package/.kxm/agents/{implementer.yaml → writer.yaml} +2 -0
- package/.kxm/roles/planner.yaml +4 -2
- package/.kxm/roles/reviewer-arch.yaml +4 -2
- package/.kxm/roles/reviewer-cli.yaml +4 -2
- package/.kxm/roles/writer.yaml +6 -3
- package/.kxm/workflows/default.yaml +20 -12
- package/.kxm/workflows/land.yaml +1 -1
- package/.kxm/workflows/{review-arch-only.yaml → reviewer-arch-only.yaml} +7 -3
- package/.kxm/workflows/{review-cli-only.yaml → reviewer-cli-only.yaml} +7 -3
- package/.kxm/workflows/{implement-only.yaml → writer-only.yaml} +8 -4
- package/CHANGELOG.md +22 -5
- package/docs/contributing/harness-routing-internals.md +17 -11
- package/docs/reference/cli-reference.md +23 -24
- package/docs/reference/config-reference.md +69 -62
- package/docs/reference/harness-routing.md +6 -5
- package/docs/reference/workflow-catalog.md +3 -3
- package/package.json +3 -3
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +1144 -804
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +636 -308
- package/plugins/kxm/dist/runtime.js +707 -379
- package/plugins/kxm/dist/server.js +49 -5
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +3 -3
- package/plugins/kxm/src/cli/project.ts +2 -1
- package/plugins/kxm/src/cli/roles.ts +3 -4
- package/plugins/kxm/src/engine.ts +9 -6
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/policy-draft.mjs +11 -5
- package/plugins/kxm/src/project-config.ts +49 -11
- package/plugins/kxm/src/runtime-service.ts +2 -1
- package/plugins/kxm/src/template.ts +39 -10
- package/plugins/kxm/src/workflow-manager.ts +34 -28
- package/plugins/kxm/src/workforce-names.d.mts +38 -0
- package/plugins/kxm/src/workforce-names.mjs +317 -0
- package/schemas/agent.schema.json +7 -0
- package/schemas/model.schema.json +12 -0
- package/schemas/workflow.schema.json +14 -0
- package/scripts/harness-run.mjs +1 -1
- package/scripts/native-critic.mjs +5 -5
- package/scripts/roster-policy.mjs +9 -3
- package/scripts/workforce-lint.mjs +18 -0
|
@@ -14958,6 +14958,7 @@ var MODES = Object.freeze(["headless", "interactive", "either"]);
|
|
|
14958
14958
|
var MODEL_KEYS = Object.freeze([
|
|
14959
14959
|
"schema",
|
|
14960
14960
|
"id",
|
|
14961
|
+
"aliases",
|
|
14961
14962
|
"harness",
|
|
14962
14963
|
"model",
|
|
14963
14964
|
"vendor",
|
|
@@ -14996,8 +14997,51 @@ var FALLBACK_REVERT = Object.freeze(["next_run", "never"]);
|
|
|
14996
14997
|
var ROSTER_ENTRY_KEYS = Object.freeze(["route", "effort", "mode"]);
|
|
14997
14998
|
var CRITIC_PURPOSES = Object.freeze(["reviewer-arch", "reviewer-cli"]);
|
|
14998
14999
|
|
|
14999
|
-
// plugins/kxm/src/
|
|
15000
|
+
// plugins/kxm/src/workforce-names.mjs
|
|
15000
15001
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
15002
|
+
var ROLE_IDS = Object.freeze([
|
|
15003
|
+
"writer",
|
|
15004
|
+
"planner",
|
|
15005
|
+
"reviewer-arch",
|
|
15006
|
+
"reviewer-cli",
|
|
15007
|
+
"experiment"
|
|
15008
|
+
]);
|
|
15009
|
+
var ROUTE_RENAMES = Object.freeze([
|
|
15010
|
+
["grok-native", "grok-grok-4-7"],
|
|
15011
|
+
["qwen-openrouter-pi", "pi-qwen3-coder-plus-openrouter"],
|
|
15012
|
+
["gemini-agy", "agy-gemini-3-8-flash-high"],
|
|
15013
|
+
["fable-claude", "claude-fable"],
|
|
15014
|
+
["sol-codex", "codex-gpt-5-6-sol"],
|
|
15015
|
+
["grok-default", "grok-grok-4-6"],
|
|
15016
|
+
["fable-default", "claude-fable"]
|
|
15017
|
+
]);
|
|
15018
|
+
var AGENT_RENAMES = Object.freeze([
|
|
15019
|
+
["coordinator", "planner"],
|
|
15020
|
+
["implementer", "writer"],
|
|
15021
|
+
["critic-arch", "reviewer-arch"],
|
|
15022
|
+
["critic-cli", "reviewer-cli"]
|
|
15023
|
+
]);
|
|
15024
|
+
var WORKFLOW_RENAMES = Object.freeze([
|
|
15025
|
+
["implement-only", "writer-only"],
|
|
15026
|
+
["review-arch-only", "reviewer-arch-only"],
|
|
15027
|
+
["review-cli-only", "reviewer-cli-only"]
|
|
15028
|
+
]);
|
|
15029
|
+
var STEP_RENAMES = Object.freeze([
|
|
15030
|
+
["implement", "writer"],
|
|
15031
|
+
["critic-arch", "reviewer-arch"],
|
|
15032
|
+
["review-arch", "reviewer-arch"],
|
|
15033
|
+
["critic-cli", "reviewer-cli"],
|
|
15034
|
+
["review-cli", "reviewer-cli"]
|
|
15035
|
+
]);
|
|
15036
|
+
var RENAMES = Object.freeze({
|
|
15037
|
+
route: ROUTE_RENAMES,
|
|
15038
|
+
agent: AGENT_RENAMES,
|
|
15039
|
+
workflow: WORKFLOW_RENAMES,
|
|
15040
|
+
step: STEP_RENAMES
|
|
15041
|
+
});
|
|
15042
|
+
|
|
15043
|
+
// plugins/kxm/src/template.ts
|
|
15044
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
15001
15045
|
|
|
15002
15046
|
// plugins/kxm/src/repo-root.ts
|
|
15003
15047
|
import { existsSync } from "node:fs";
|
|
@@ -18253,7 +18297,7 @@ function explainContextItem(id, pool) {
|
|
|
18253
18297
|
}
|
|
18254
18298
|
|
|
18255
18299
|
// plugins/kxm/src/memory.ts
|
|
18256
|
-
var
|
|
18300
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
18257
18301
|
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
18258
18302
|
import { extname as extname2, join as join3, resolve as resolve2 } from "node:path";
|
|
18259
18303
|
var MEMORY_SCHEMA = "kxm.memory.v1";
|
|
@@ -18285,7 +18329,7 @@ function parseFrontmatter(content) {
|
|
|
18285
18329
|
}
|
|
18286
18330
|
function parseMemoryRecord(raw, filename = "memory.md") {
|
|
18287
18331
|
const { frontmatter, body } = parseFrontmatter(raw);
|
|
18288
|
-
const data = (0,
|
|
18332
|
+
const data = (0, import_yaml4.parse)(frontmatter);
|
|
18289
18333
|
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
18290
18334
|
throw new Error(`invalid YAML frontmatter in ${filename}`);
|
|
18291
18335
|
}
|
|
@@ -18622,7 +18666,7 @@ function boundedRefs(value, field) {
|
|
|
18622
18666
|
}
|
|
18623
18667
|
|
|
18624
18668
|
// plugins/kxm/src/skills.ts
|
|
18625
|
-
var
|
|
18669
|
+
var import_yaml5 = __toESM(require_dist(), 1);
|
|
18626
18670
|
import { createHash as createHash4 } from "node:crypto";
|
|
18627
18671
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
18628
18672
|
import { dirname as dirname4, join as join4 } from "node:path";
|
|
@@ -18666,7 +18710,7 @@ function parseSkillFrontmatter(content) {
|
|
|
18666
18710
|
return { frontmatter: null, body: content };
|
|
18667
18711
|
}
|
|
18668
18712
|
try {
|
|
18669
|
-
const parsed = (0,
|
|
18713
|
+
const parsed = (0, import_yaml5.parse)(rawFm);
|
|
18670
18714
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
18671
18715
|
return { frontmatter: parsed, body: rawBody };
|
|
18672
18716
|
}
|
package/plugins/kxm/package.json
CHANGED
|
@@ -22,7 +22,7 @@ to run in their own terminal or in Claude Code.
|
|
|
22
22
|
`ready`, `repair`, or `legacy`, and `issues` lists anything to fix first.
|
|
23
23
|
2. `kxm init --name "<display name>"` prints
|
|
24
24
|
`initialized KXM project at <root>`. It writes `.kxm/project.yaml`,
|
|
25
|
-
`.kxm/agents/
|
|
25
|
+
`.kxm/agents/planner.yaml`, `.kxm/agents/writer.yaml`,
|
|
26
26
|
`.kxm/gates.yaml`, `.kxm/repo/repo.yaml`, `.kxm/template-provenance.yaml`,
|
|
27
27
|
and `.kxm/workflows/default.yaml`. Outside Git it fails with
|
|
28
28
|
`git_root_required`. In an existing project, plain `kxm init` validates,
|
|
@@ -44,7 +44,7 @@ After the user's commit:
|
|
|
44
44
|
1. `kxm workflow add first --template spec-and-plan` prints
|
|
45
45
|
`Added workflow 'first' to local (<root>/.kxm/workflows/first.yaml)`. Add
|
|
46
46
|
`--dry-run` first to see the path without writing. The template has two
|
|
47
|
-
steps, `plan` then `
|
|
47
|
+
steps, `plan` then `reviewer-arch`, both run by the `planner` agent with
|
|
48
48
|
`repositories: control: read`. It writes nothing and runs no test command,
|
|
49
49
|
and a failed review goes back to `plan` at most twice.
|
|
50
50
|
2. `kxm init` validates it and prints `validated KXM project at <root>`.
|
|
@@ -71,7 +71,7 @@ After the user's commit:
|
|
|
71
71
|
4. `kxm runs status <runId>` prints `created`.
|
|
72
72
|
5. `kxm runs drive <runId> --simulated --wait --timeout-ms 60000` prints a
|
|
73
73
|
`kxm.drive-receipt.v1` whose settlement is terminal `completed`, and exits 0.
|
|
74
|
-
The run moves from `plan` to `
|
|
74
|
+
The run moves from `plan` to `reviewer-arch` to `completed`. Always pass
|
|
75
75
|
`--simulated`; without it, drive calls live harnesses.
|
|
76
76
|
6. `kxm runs status <runId>` prints `completed … (receipt verified)`.
|
|
77
77
|
7. `kxm runs receipt <runId>` and `kxm runs list`. Cancel a stuck run with
|
|
@@ -11,6 +11,7 @@ import { runVisionGate } from "../vision-gate.ts";
|
|
|
11
11
|
import {
|
|
12
12
|
KxmConfigError,
|
|
13
13
|
discoverKxmProjectRoot,
|
|
14
|
+
lookupKxmResource,
|
|
14
15
|
type KxmInitializationPlan,
|
|
15
16
|
} from "../project-config.ts";
|
|
16
17
|
import { initializeKxmProject } from "../init.ts";
|
|
@@ -439,7 +440,7 @@ export function resolveKxmRunTarget(
|
|
|
439
440
|
}
|
|
440
441
|
const bundle = loadKxmProject(projectRoot, {});
|
|
441
442
|
const workflowId = workflow ?? String(bundle.project.value.defaultWorkflow ?? "default");
|
|
442
|
-
if (!bundle.workflows.
|
|
443
|
+
if (!lookupKxmResource(bundle.workflows.values(), workflowId, "workflow")) {
|
|
443
444
|
print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow: workflowId, ...envelope }, `workflow ${workflowId} does not exist in this project`);
|
|
444
445
|
return 1;
|
|
445
446
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type KxmRoleDefinition,
|
|
16
16
|
} from "../role.ts";
|
|
17
17
|
import { discoverKxmProjectRoot, kxmRoleWriteIssues } from "../project-config.ts";
|
|
18
|
+
import { findYamlBasename } from "../workforce-names.mjs";
|
|
18
19
|
import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
|
|
19
20
|
import { projectRuntimeOwnsRun } from "../runtime-store.ts";
|
|
20
21
|
import { resumeWorkflowFromRuling, type WorkflowRun } from "../workflow.ts";
|
|
@@ -185,8 +186,7 @@ export async function cmdRoleAdd(
|
|
|
185
186
|
// after its roster is parsed, for both local and global scope.
|
|
186
187
|
const projectRoot = discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
187
188
|
const refuseMissingRoute = (routeId: string): boolean => {
|
|
188
|
-
|
|
189
|
-
if (existsSync(modelFile)) return false;
|
|
189
|
+
if (findYamlBasename(join(projectRoot, ".kxm", "models"), routeId, "route")) return false;
|
|
190
190
|
runtime.io.stderr(`kxm: route '${routeId}' is not a file under .kxm/models/\n`);
|
|
191
191
|
return true;
|
|
192
192
|
};
|
|
@@ -398,8 +398,7 @@ export async function cmdRoleModify(
|
|
|
398
398
|
if (options.addRoute) {
|
|
399
399
|
const routeId = options.addRoute;
|
|
400
400
|
const projectRoot = discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
401
|
-
|
|
402
|
-
if (!existsSync(modelFile)) {
|
|
401
|
+
if (!findYamlBasename(join(projectRoot, ".kxm", "models"), routeId, "route")) {
|
|
403
402
|
runtime.io.stderr(`kxm: route '${routeId}' is not a file under .kxm/models/\n`);
|
|
404
403
|
return 1;
|
|
405
404
|
}
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import { listRoleBindings, loadRoutePolicy } from "./routes.ts";
|
|
6
|
+
import { findYamlBasename } from "./workforce-names.mjs";
|
|
6
7
|
import { applyAuthoringWitness, captureWorktreeWitness } from "./worktree-witness.ts";
|
|
7
8
|
import {
|
|
8
9
|
buildFormalContextPacket,
|
|
@@ -101,7 +102,7 @@ import {
|
|
|
101
102
|
type KxmRunRecord,
|
|
102
103
|
type KxmRunStatus,
|
|
103
104
|
} from "./runtime-store.ts";
|
|
104
|
-
import { KxmConfigError, kxmCanonicalJson, type JsonValue, type KxmProjectBundle } from "./project-config.ts";
|
|
105
|
+
import { KxmConfigError, kxmCanonicalJson, lookupKxmResource, type JsonValue, type KxmProjectBundle } from "./project-config.ts";
|
|
105
106
|
import { evaluateArtifactsGate } from "./engine-artifacts.ts";
|
|
106
107
|
import { createCommandObserver } from "./engine-command.ts";
|
|
107
108
|
import {
|
|
@@ -277,7 +278,7 @@ export function pinKxmCompiledPlan(
|
|
|
277
278
|
if (String(bundle.project.value.id) !== run.projectId) {
|
|
278
279
|
throw runtimeError("run_owner_mismatch", runId, "bundle project does not match the run");
|
|
279
280
|
}
|
|
280
|
-
const workflow = bundle.workflows.
|
|
281
|
+
const workflow = lookupKxmResource(bundle.workflows.values(), run.workflowId, "workflow");
|
|
281
282
|
if (!workflow) throw runtimeError("run_workflow_unknown", run.workflowId, `workflow ${run.workflowId} does not exist in this project`);
|
|
282
283
|
const revisions = kxmPolicyRevisions(bundle, options);
|
|
283
284
|
if (
|
|
@@ -1397,7 +1398,8 @@ function resolveProducerRoute(
|
|
|
1397
1398
|
},
|
|
1398
1399
|
};
|
|
1399
1400
|
}
|
|
1400
|
-
const
|
|
1401
|
+
const agentFile = findYamlBasename(join(projectRoot, ".kxm", "agents"), agentId, "agent") ?? agentId;
|
|
1402
|
+
const agent = readYamlFile(join(projectRoot, ".kxm", "agents", `${agentFile}.yaml`));
|
|
1401
1403
|
const role = typeof agent?.role === "string" ? agent.role : "";
|
|
1402
1404
|
if (!role) {
|
|
1403
1405
|
return {
|
|
@@ -1424,7 +1426,8 @@ function resolveProducerRoute(
|
|
|
1424
1426
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
1425
1427
|
const record = entry as { route?: unknown; effort?: unknown };
|
|
1426
1428
|
if (typeof record.route !== "string" || record.route.length === 0) continue;
|
|
1427
|
-
const
|
|
1429
|
+
const routeFile = findYamlBasename(join(projectRoot, ".kxm", "models"), record.route, "route") ?? record.route;
|
|
1430
|
+
const modelPath = join(projectRoot, ".kxm", "models", `${routeFile}.yaml`);
|
|
1428
1431
|
const doc = readModelDocument(modelPath);
|
|
1429
1432
|
if (!doc) {
|
|
1430
1433
|
return {
|
|
@@ -1487,7 +1490,7 @@ function resolveProducerRoute(
|
|
|
1487
1490
|
model: selector.slice(slash + 1),
|
|
1488
1491
|
selector,
|
|
1489
1492
|
harness,
|
|
1490
|
-
routeId:
|
|
1493
|
+
routeId: routeFile,
|
|
1491
1494
|
role,
|
|
1492
1495
|
permissions,
|
|
1493
1496
|
...(typeof record.effort === "string" ? { effort: record.effort } : {}),
|
|
@@ -2871,7 +2874,7 @@ export function kxmLiveRunPrerequisites(
|
|
|
2871
2874
|
workflowId: string,
|
|
2872
2875
|
projectRoot: string,
|
|
2873
2876
|
): KxmRunHandoff[] {
|
|
2874
|
-
const workflow = bundle.workflows.
|
|
2877
|
+
const workflow = lookupKxmResource(bundle.workflows.values(), workflowId, "workflow");
|
|
2875
2878
|
if (!workflow) throw runtimeError("run_workflow_unknown", workflowId, `workflow ${workflowId} does not exist in this project`);
|
|
2876
2879
|
const plan = compileKxmWorkflow({ id: workflowId, value: workflow.value, logicalPath: workflow.logicalPath });
|
|
2877
2880
|
const envelope = { plan, projectLimits: kxmProjectAdmissionLimits(bundle), gates: pinnedGatesForCompiledPlan(plan, bundle, projectRoot) };
|
|
@@ -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.146";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -20,7 +20,7 @@ const TOKEN = /[\s\x00-\x1f]/u;
|
|
|
20
20
|
const EFFORTS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
21
21
|
const MODES = Object.freeze(["headless", "interactive", "either"]);
|
|
22
22
|
const MODEL_KEYS = Object.freeze([
|
|
23
|
-
"schema", "id", "harness", "model", "vendor", "status", "permissions", "origin",
|
|
23
|
+
"schema", "id", "aliases", "harness", "model", "vendor", "status", "permissions", "origin",
|
|
24
24
|
"thinking", "tags", "capabilities", "priority", "fallbacks", "limits",
|
|
25
25
|
]);
|
|
26
26
|
const ROLE_KEYS = Object.freeze([
|
|
@@ -226,6 +226,9 @@ function validateModelShape(document, id, label, issues) {
|
|
|
226
226
|
if (document.id !== undefined && document.id !== id) {
|
|
227
227
|
issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`));
|
|
228
228
|
}
|
|
229
|
+
if (document.aliases !== undefined && !identifierList(document.aliases)) {
|
|
230
|
+
issues.push(issue("schema", "schema_pattern", label, "aliases must be unique identifiers"));
|
|
231
|
+
}
|
|
229
232
|
if (document.harness !== undefined && !identifier(document.harness)) {
|
|
230
233
|
issues.push(issue("schema", "schema_pattern", label, "harness must be an identifier"));
|
|
231
234
|
}
|
|
@@ -511,9 +514,12 @@ function validateRoleSemantics(role, id, label, models, options, issues) {
|
|
|
511
514
|
const admitted = role.roster
|
|
512
515
|
.map((entry) => models.get(entry.route))
|
|
513
516
|
.filter((model) => model && admittedRoute(model));
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
+
// The first roster entry is the required critic. Later entries are
|
|
518
|
+
// fallbacks so one provider outage does not stop the role. Every entry
|
|
519
|
+
// stays read-only; vendor independence uses the first entry only.
|
|
520
|
+
if (admitted.length < 1) {
|
|
521
|
+
issues.push(issue("semantic", "critic_route_count", label, "required critic purpose files must list at least one admitted route"));
|
|
522
|
+
} else if (role.permission !== "read-only" || admitted.some((model) => model.permissions.length !== 1 || model.permissions[0] !== "read-only")) {
|
|
517
523
|
issues.push(issue("semantic", "permission_escalation", label, "critic purpose files must be read-only"));
|
|
518
524
|
}
|
|
519
525
|
}
|
|
@@ -549,7 +555,7 @@ function validateCriticVendors(roles, models, options, issues) {
|
|
|
549
555
|
const admitted = (role.roster ?? [])
|
|
550
556
|
.map((entry) => models.get(entry.route))
|
|
551
557
|
.filter((model) => model && admittedRoute(model));
|
|
552
|
-
if (admitted.length
|
|
558
|
+
if (admitted.length >= 1) critics.push({ purpose, vendor: canonicalVendor(admitted[0].vendor, aliases) });
|
|
553
559
|
}
|
|
554
560
|
if (critics.length === 2 && critics[0].vendor === critics[1].vendor) {
|
|
555
561
|
issues.push(issue("semantic", "critic_vendor_collision", "roles/reviewer-arch", "required critics must have independent vendors"));
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./restricted-yaml.mjs";
|
|
12
12
|
import { pathToFileURL } from "node:url";
|
|
13
13
|
import { validatePolicyDraft } from "./policy-draft.mjs";
|
|
14
|
+
import { findYamlBasename, lintWorkforce, lookupById, noteDeprecatedId, STEP_RENAMES } from "./workforce-names.mjs";
|
|
14
15
|
import { resolveKxmTemplateBaseline } from "./template.ts";
|
|
15
16
|
import { findKxmRepoRoot } from "./repo-root.ts";
|
|
16
17
|
import { BUILTIN_HARNESS_IDS, DEFAULT_HARNESS } from "./harness.ts";
|
|
@@ -590,6 +591,20 @@ function names(value: JsonValue | undefined): string[] {
|
|
|
590
591
|
return Array.isArray(value) ? value.filter((candidate): candidate is string => typeof candidate === "string") : [];
|
|
591
592
|
}
|
|
592
593
|
|
|
594
|
+
/** Resolve an agent, workflow, or route, including a deprecated alias. */
|
|
595
|
+
export function lookupKxmResource<T extends { id?: string; value: JsonObject }>(
|
|
596
|
+
resources: Iterable<T>,
|
|
597
|
+
requested: string,
|
|
598
|
+
kind: "agent" | "workflow" | "route",
|
|
599
|
+
): T | undefined {
|
|
600
|
+
const records = [...resources].map((resource) => ({
|
|
601
|
+
id: resource.id,
|
|
602
|
+
aliases: names(resource.value.aliases),
|
|
603
|
+
resource,
|
|
604
|
+
}));
|
|
605
|
+
return lookupById(records, requested, kind)?.record.resource;
|
|
606
|
+
}
|
|
607
|
+
|
|
593
608
|
function mapResource(resource: KxmResource, map: Map<string, KxmResource>, issues: KxmConfigIssue[]): void {
|
|
594
609
|
const id = resource.id;
|
|
595
610
|
if (!id) return;
|
|
@@ -872,14 +887,31 @@ function validateWorkflow(
|
|
|
872
887
|
): void {
|
|
873
888
|
const file = workflow.logicalPath;
|
|
874
889
|
const coordinator = stringValue(workflow.value.coordinator) ?? "coordinator";
|
|
875
|
-
if (!agents.
|
|
890
|
+
if (!lookupKxmResource(agents.values(), coordinator, "agent")) issues.push(issue("reference", "coordinator_unknown", file, `coordinator ${coordinator} does not exist`));
|
|
876
891
|
const steps = valuesOf(workflow.value, "steps").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate));
|
|
877
892
|
const stepIndex = new Map<string, number>();
|
|
893
|
+
const primarySteps = new Set<string>();
|
|
878
894
|
for (const [index, step] of steps.entries()) {
|
|
879
895
|
const id = stringValue(step.id);
|
|
880
896
|
if (!id) continue;
|
|
881
897
|
if (stepIndex.has(id)) issues.push(issue("semantic", "step_id_duplicate", file, `step ${id} is duplicated`));
|
|
882
|
-
else
|
|
898
|
+
else {
|
|
899
|
+
stepIndex.set(id, index);
|
|
900
|
+
primarySteps.add(id);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
for (const [index, step] of steps.entries()) {
|
|
904
|
+
const id = stringValue(step.id);
|
|
905
|
+
if (!id) continue;
|
|
906
|
+
const aliases = new Set(names(step.aliases));
|
|
907
|
+
for (const [oldId, newId] of STEP_RENAMES) {
|
|
908
|
+
if (id === newId) aliases.add(oldId);
|
|
909
|
+
if (id === oldId) aliases.add(newId);
|
|
910
|
+
}
|
|
911
|
+
for (const alias of aliases) {
|
|
912
|
+
if (alias === id || primarySteps.has(alias) || stepIndex.has(alias)) continue;
|
|
913
|
+
stepIndex.set(alias, index);
|
|
914
|
+
}
|
|
883
915
|
}
|
|
884
916
|
|
|
885
917
|
for (const field of ["reproOracle", "planHash"] as const) {
|
|
@@ -910,7 +942,7 @@ function validateWorkflow(
|
|
|
910
942
|
const kind = stringValue(step.kind);
|
|
911
943
|
validateToolPolicy(workflow, objectValue(step.tools), `${stepId}.tools`, issues);
|
|
912
944
|
const primaryAgentId = stringValue(step.agent);
|
|
913
|
-
if ((kind === "agent" || kind === "moa") && primaryAgentId && !agents.
|
|
945
|
+
if ((kind === "agent" || kind === "moa") && primaryAgentId && !lookupKxmResource(agents.values(), primaryAgentId, "agent")) {
|
|
914
946
|
issues.push(issue("reference", "agent_unknown", file, `${stepId} references unknown agent ${primaryAgentId}`));
|
|
915
947
|
}
|
|
916
948
|
const gate = stringValue(step.gate);
|
|
@@ -938,7 +970,7 @@ function validateWorkflow(
|
|
|
938
970
|
const declaredAgents = assignment ? names(assignment.allowedAgents) : [];
|
|
939
971
|
const allowedAgents = declaredAgents.length > 0 ? declaredAgents : primaryAgentId ? [primaryAgentId] : [];
|
|
940
972
|
for (const agentId of allowedAgents) {
|
|
941
|
-
const agent = agents.
|
|
973
|
+
const agent = lookupKxmResource(agents.values(), agentId, "agent");
|
|
942
974
|
if (!agent) issues.push(issue("reference", "assignment_agent_unknown", file, `${stepId} allows unknown agent ${agentId}`));
|
|
943
975
|
else {
|
|
944
976
|
validateAgentScope(agent, step, repositories, file, stepId, issues);
|
|
@@ -1003,7 +1035,7 @@ function validateWorkflow(
|
|
|
1003
1035
|
const producerMinimum = numberValue(policy.minimumProducers, 1);
|
|
1004
1036
|
if (producerMinimum > target || producerMinimum > eligible.length) issues.push(issue("semantic", "producer_minimum_impossible", file, `${stepId}.${String(key)} producer minimum exceeds its eligible or target pool`));
|
|
1005
1037
|
for (const agentId of eligible) {
|
|
1006
|
-
if (!agents.
|
|
1038
|
+
if (!lookupKxmResource(agents.values(), agentId, "agent")) issues.push(issue("reference", "producer_agent_unknown", file, `${stepId}.${String(key)} references unknown producer ${agentId}`));
|
|
1007
1039
|
if (allowedAgents.length > 0 && !allowedAgents.includes(agentId)) issues.push(issue("semantic", "producer_agent_ineligible", file, `${stepId}.${String(key)} producer ${agentId} is outside allowedAgents`));
|
|
1008
1040
|
}
|
|
1009
1041
|
const degradation = objectValue(policy.degradation);
|
|
@@ -1026,6 +1058,10 @@ function validateWorkflow(
|
|
|
1026
1058
|
if (parsed.terminalStatus === "completed") completedTerminalSources.add(index);
|
|
1027
1059
|
continue;
|
|
1028
1060
|
}
|
|
1061
|
+
if (parsed.target && parsed.target !== "$terminal" && !primarySteps.has(parsed.target) && stepIndex.has(parsed.target)) {
|
|
1062
|
+
const canonical = stringValue(steps[stepIndex.get(parsed.target) ?? -1]?.id);
|
|
1063
|
+
if (canonical) noteDeprecatedId("step", parsed.target, canonical);
|
|
1064
|
+
}
|
|
1029
1065
|
const targetIndex = parsed.target === undefined ? undefined : stepIndex.get(parsed.target);
|
|
1030
1066
|
if (targetIndex === undefined) {
|
|
1031
1067
|
issues.push(issue("reference", "transition_target_unknown", file, `${stepId}.${outcome} references unknown target ${String(parsed.target)}`));
|
|
@@ -1195,7 +1231,7 @@ function validateBundle(
|
|
|
1195
1231
|
validateModelReferences(models, issues);
|
|
1196
1232
|
|
|
1197
1233
|
const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
|
|
1198
|
-
if (!workflows.
|
|
1234
|
+
if (!lookupKxmResource(workflows.values(), defaultWorkflow, "workflow")) issues.push(issue("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
|
|
1199
1235
|
for (const workflow of workflows.values()) validateWorkflow(workflow, agents, models, repositoryIds, gates, issues);
|
|
1200
1236
|
|
|
1201
1237
|
// Developer policy is the role files under .kxm/roles/ (with the model
|
|
@@ -1220,8 +1256,9 @@ function rosterModelIssues(projectRoot: string, roles: ReadonlyMap<string, KxmRe
|
|
|
1220
1256
|
for (const entry of valuesOf(role.value, "roster")) {
|
|
1221
1257
|
const route = stringValue(objectValue(entry)?.route);
|
|
1222
1258
|
if (!route) continue;
|
|
1223
|
-
const
|
|
1224
|
-
const
|
|
1259
|
+
const resolved = findYamlBasename(join(projectRoot, ".kxm", "models"), route, "route") ?? route;
|
|
1260
|
+
const logical = `.kxm/models/${resolved}.yaml`;
|
|
1261
|
+
const file = join(projectRoot, ".kxm", "models", `${resolved}.yaml`);
|
|
1225
1262
|
let harness: string | undefined;
|
|
1226
1263
|
try {
|
|
1227
1264
|
if (existsSync(file)) harness = stringValue(parseRestrictedYaml(readFileSync(file, "utf8"), logical).harness);
|
|
@@ -1305,6 +1342,7 @@ function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
|
|
|
1305
1342
|
// is this package's developer policy, so it runs when the loaded project
|
|
1306
1343
|
// is that package.
|
|
1307
1344
|
if (resolve(projectRoot) !== findKxmRepoRoot(import.meta.url)) return [];
|
|
1345
|
+
const lintIssues = lintWorkforce(projectRoot).errors.map((entry) => issue("semantic", entry.code, entry.file, entry.message));
|
|
1308
1346
|
const rolesDir = join(projectRoot, ".kxm", "roles");
|
|
1309
1347
|
const modelsDir = join(projectRoot, ".kxm", "models");
|
|
1310
1348
|
const roles: Record<string, JsonObject> = {};
|
|
@@ -1340,7 +1378,7 @@ function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
|
|
|
1340
1378
|
}
|
|
1341
1379
|
const ceilings = developerCeilings();
|
|
1342
1380
|
if (!ceilings.ok) {
|
|
1343
|
-
return [...parseIssues, issue("semantic", "developer_ceilings_unavailable", ".kxm/models", `developer ceilings could not be loaded: ${ceilings.detail}`)];
|
|
1381
|
+
return [...parseIssues, ...lintIssues, issue("semantic", "developer_ceilings_unavailable", ".kxm/models", `developer ceilings could not be loaded: ${ceilings.detail}`)];
|
|
1344
1382
|
}
|
|
1345
1383
|
const result = validatePolicyDraft({ models, roles, evidence }, {
|
|
1346
1384
|
ceilings: ceilings.ROUTES,
|
|
@@ -1349,8 +1387,8 @@ function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
|
|
|
1349
1387
|
piNativeVendorProviders: ceilings.PI_NATIVE_VENDOR_PROVIDERS,
|
|
1350
1388
|
vendorAliases: { "x-ai": "xai", moonshotai: "moonshot", "google-ai": "google", qwen: "alibaba" },
|
|
1351
1389
|
});
|
|
1352
|
-
if (result.ok) return parseIssues;
|
|
1353
|
-
return [...parseIssues, ...result.issues.map((entry) => issue(entry.phase, entry.code, entry.file, entry.message))];
|
|
1390
|
+
if (result.ok) return [...parseIssues, ...lintIssues];
|
|
1391
|
+
return [...parseIssues, ...lintIssues, ...result.issues.map((entry) => issue(entry.phase, entry.code, entry.file, entry.message))];
|
|
1354
1392
|
}
|
|
1355
1393
|
|
|
1356
1394
|
export function kxmCanonicalJson(value: JsonValue): string {
|
|
@@ -5,6 +5,7 @@ import type { ContextItem } from "./context.ts";
|
|
|
5
5
|
import {
|
|
6
6
|
KxmConfigError,
|
|
7
7
|
loadKxmProject,
|
|
8
|
+
lookupKxmResource,
|
|
8
9
|
kxmCanonicalJson,
|
|
9
10
|
type JsonObject,
|
|
10
11
|
type JsonValue,
|
|
@@ -402,7 +403,7 @@ export function acceptKxmRun(
|
|
|
402
403
|
const now = options.now ?? new Date().toISOString();
|
|
403
404
|
const monotonicNs = options.monotonicNs ?? kxmMonotonicNs();
|
|
404
405
|
|
|
405
|
-
const workflow = bundle.workflows.
|
|
406
|
+
const workflow = lookupKxmResource(bundle.workflows.values(), request.workflowId, "workflow");
|
|
406
407
|
if (!workflow) {
|
|
407
408
|
throw runtimeError("run_workflow_unknown", ".kxm/workflows", `workflow ${request.workflowId} does not exist in this project`);
|
|
408
409
|
}
|
|
@@ -175,35 +175,64 @@ function coreTemplate(projectId: string, projectName: string, variant: KxmTempla
|
|
|
175
175
|
]);
|
|
176
176
|
if (variant === "v4-registry") {
|
|
177
177
|
const coordinator = files.get(".kxm/agents/coordinator.yaml");
|
|
178
|
-
if (coordinator)
|
|
178
|
+
if (coordinator) {
|
|
179
|
+
files.delete(".kxm/agents/coordinator.yaml");
|
|
180
|
+
files.set(".kxm/agents/planner.yaml", { ...coordinator, role: "planner", aliases: ["coordinator"] });
|
|
181
|
+
}
|
|
179
182
|
const implementer = files.get(".kxm/agents/implementer.yaml");
|
|
180
|
-
if (implementer)
|
|
183
|
+
if (implementer) {
|
|
184
|
+
files.delete(".kxm/agents/implementer.yaml");
|
|
185
|
+
files.set(".kxm/agents/writer.yaml", { ...implementer, role: "writer", aliases: ["implementer"] });
|
|
186
|
+
}
|
|
181
187
|
const workflow = files.get(".kxm/workflows/default.yaml");
|
|
182
188
|
if (workflow) {
|
|
183
189
|
const limits = { ...(workflow.limits as JsonObject) };
|
|
184
190
|
delete limits.maxAgentTimeMs;
|
|
185
|
-
|
|
191
|
+
const steps = (workflow.steps as JsonObject[]).map((step) => {
|
|
192
|
+
if (step.id === "plan") {
|
|
193
|
+
const on = { ...(step.on as JsonObject), passed: "writer" };
|
|
194
|
+
return { ...step, agent: "planner", on };
|
|
195
|
+
}
|
|
196
|
+
if (step.id === "implement") {
|
|
197
|
+
const assignments = { ...(step.assignments as JsonObject), allowedAgents: ["writer"] };
|
|
198
|
+
return { ...step, id: "writer", aliases: ["implement"], agent: "writer", assignments };
|
|
199
|
+
}
|
|
200
|
+
if (step.id === "verify") {
|
|
201
|
+
const on = { ...(step.on as JsonObject) };
|
|
202
|
+
const failure = on["implementation-failure"];
|
|
203
|
+
if (failure && typeof failure === "object" && !Array.isArray(failure)) {
|
|
204
|
+
on["implementation-failure"] = { ...(failure as JsonObject), target: "writer" };
|
|
205
|
+
}
|
|
206
|
+
return { ...step, on };
|
|
207
|
+
}
|
|
208
|
+
return step;
|
|
209
|
+
});
|
|
210
|
+
files.set(".kxm/workflows/default.yaml", { ...workflow, coordinator: "planner", limits, steps });
|
|
186
211
|
}
|
|
187
212
|
// The two models the template names, and nothing else. A fresh project can
|
|
188
213
|
// be driven without falling through to an unadmitted default model.
|
|
214
|
+
// Route ids follow <harness>-<model-slug>. grok-default and fable-default
|
|
215
|
+
// remain aliases so an existing --route flag still resolves.
|
|
189
216
|
files.set(".kxm/routes.yaml", {
|
|
190
217
|
schema: "kxm.routes.v2",
|
|
191
|
-
updatedAt: "2026-09-
|
|
218
|
+
updatedAt: "2026-09-27T00:00:00.000Z",
|
|
192
219
|
admitted: ["anthropic/fable", "xai/grok-4.6"],
|
|
193
220
|
disabled: [],
|
|
194
221
|
});
|
|
195
|
-
files.set(".kxm/models/grok-
|
|
222
|
+
files.set(".kxm/models/grok-grok-4-6.yaml", {
|
|
196
223
|
schema: "kxm.model.v2",
|
|
197
|
-
id: "grok-
|
|
224
|
+
id: "grok-grok-4-6",
|
|
225
|
+
aliases: ["grok-default"],
|
|
198
226
|
harness: "grok",
|
|
199
227
|
model: "grok-4.6",
|
|
200
228
|
vendor: "xai",
|
|
201
229
|
status: "admitted",
|
|
202
230
|
permissions: ["edit"],
|
|
203
231
|
});
|
|
204
|
-
files.set(".kxm/models/fable
|
|
232
|
+
files.set(".kxm/models/claude-fable.yaml", {
|
|
205
233
|
schema: "kxm.model.v2",
|
|
206
|
-
id: "fable
|
|
234
|
+
id: "claude-fable",
|
|
235
|
+
aliases: ["fable-default"],
|
|
207
236
|
harness: "claude",
|
|
208
237
|
model: "fable",
|
|
209
238
|
vendor: "anthropic",
|
|
@@ -216,7 +245,7 @@ function coreTemplate(projectId: string, projectName: string, variant: KxmTempla
|
|
|
216
245
|
purpose: "writer",
|
|
217
246
|
permission: "edit",
|
|
218
247
|
description: "Primary implementation agent.",
|
|
219
|
-
roster: [{ route: "grok-
|
|
248
|
+
roster: [{ route: "grok-grok-4-6", effort: "medium" }],
|
|
220
249
|
});
|
|
221
250
|
files.set(".kxm/roles/planner.yaml", {
|
|
222
251
|
schema: "kxm.role.v2",
|
|
@@ -224,7 +253,7 @@ function coreTemplate(projectId: string, projectName: string, variant: KxmTempla
|
|
|
224
253
|
purpose: "planner",
|
|
225
254
|
permission: "read-only",
|
|
226
255
|
description: "Plans the change before implementation.",
|
|
227
|
-
roster: [{ route: "fable
|
|
256
|
+
roster: [{ route: "claude-fable", effort: "medium" }],
|
|
228
257
|
});
|
|
229
258
|
files.set(".kxm/gates.yaml", {
|
|
230
259
|
schema: "kxm.gate-registry.v1",
|