@kontextmind/kxm 0.7.145 → 0.7.147
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 +48 -8
- package/docs/README.md +1 -1
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +8 -5
- package/docs/adr/ADR-0005-obscura-default-playwright.md +4 -3
- package/docs/adr/ADR-0006-machine-account-names.md +90 -0
- package/docs/adr/ADR-0007-steel-caddy-authentik.md +97 -0
- package/docs/adr/README.md +3 -1
- package/docs/contributing/harness-routing-internals.md +17 -11
- package/docs/contributing/operating-rules.md +13 -1
- package/docs/guides/agent-skills.md +1 -1
- package/docs/guides/browser-automation.md +44 -28
- package/docs/kb/how-credentials-retrieved-safely.md +22 -23
- package/docs/kb/how-to-connect-playwright-to-steel.md +8 -5
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +11 -10
- package/docs/kb/why-automation-opened-different-browser.md +4 -3
- package/docs/operations/deploy.md +31 -0
- package/docs/operations/troubleshooting.md +23 -1
- package/docs/prompts/browser-diagnose-recover.md +2 -2
- package/docs/prompts/browser-start.md +1 -1
- package/docs/reference/cli-reference.md +23 -24
- package/docs/reference/config-reference.md +69 -62
- package/docs/reference/configuration.md +9 -7
- 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 +1145 -805
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +636 -308
- package/plugins/kxm/dist/runtime.js +710 -382
- package/plugins/kxm/dist/server.js +49 -5
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +11 -12
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +5 -4
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +4 -3
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +5 -5
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +3 -3
- package/plugins/kxm/src/browser.ts +12 -9
- 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/modes.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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workforce id conventions and rename resolution.
|
|
3
|
+
*
|
|
4
|
+
* Role ids are the vocabulary (`planner`, `writer`, `reviewer-arch`,
|
|
5
|
+
* `reviewer-cli`, `experiment`). Agent ids and agent-step ids use that same
|
|
6
|
+
* vocabulary. Route ids are `<harness>-<model-slug>[-<provider>]`, with `.`
|
|
7
|
+
* in the model slug written as `-`. A provider suffix is required when the
|
|
8
|
+
* model id contains `/`.
|
|
9
|
+
*
|
|
10
|
+
* Resolution is bidirectional and only fills a missing side. A commit whose
|
|
11
|
+
* roles and models still use the old ids keeps matching itself, which is what
|
|
12
|
+
* the assignment runner reads at one commit. A tree that has already renamed
|
|
13
|
+
* the files still accepts the old id. A mixed tree accepts either side.
|
|
14
|
+
* `opus-claude` is not renamed: that route pointed at an unadmitted model and
|
|
15
|
+
* was removed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import YAML from "yaml";
|
|
21
|
+
|
|
22
|
+
export const ROSTER_EFFORTS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
23
|
+
|
|
24
|
+
export const ROLE_IDS = Object.freeze([
|
|
25
|
+
"writer",
|
|
26
|
+
"planner",
|
|
27
|
+
"reviewer-arch",
|
|
28
|
+
"reviewer-cli",
|
|
29
|
+
"experiment",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** Old id, canonical id. Listed once; lookup tries both directions. */
|
|
33
|
+
export const ROUTE_RENAMES = Object.freeze([
|
|
34
|
+
["grok-native", "grok-grok-4-7"],
|
|
35
|
+
["qwen-openrouter-pi", "pi-qwen3-coder-plus-openrouter"],
|
|
36
|
+
["gemini-agy", "agy-gemini-3-8-flash-high"],
|
|
37
|
+
["fable-claude", "claude-fable"],
|
|
38
|
+
["sol-codex", "codex-gpt-5-6-sol"],
|
|
39
|
+
["grok-default", "grok-grok-4-6"],
|
|
40
|
+
["fable-default", "claude-fable"],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export const AGENT_RENAMES = Object.freeze([
|
|
44
|
+
["coordinator", "planner"],
|
|
45
|
+
["implementer", "writer"],
|
|
46
|
+
["critic-arch", "reviewer-arch"],
|
|
47
|
+
["critic-cli", "reviewer-cli"],
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export const WORKFLOW_RENAMES = Object.freeze([
|
|
51
|
+
["implement-only", "writer-only"],
|
|
52
|
+
["review-arch-only", "reviewer-arch-only"],
|
|
53
|
+
["review-cli-only", "reviewer-cli-only"],
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export const STEP_RENAMES = Object.freeze([
|
|
57
|
+
["implement", "writer"],
|
|
58
|
+
["critic-arch", "reviewer-arch"],
|
|
59
|
+
["review-arch", "reviewer-arch"],
|
|
60
|
+
["critic-cli", "reviewer-cli"],
|
|
61
|
+
["review-cli", "reviewer-cli"],
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const RENAMES = Object.freeze({
|
|
65
|
+
route: ROUTE_RENAMES,
|
|
66
|
+
agent: AGENT_RENAMES,
|
|
67
|
+
workflow: WORKFLOW_RENAMES,
|
|
68
|
+
step: STEP_RENAMES,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const RESERVED_WORKFLOW_IDS = new Set(["default", "land"]);
|
|
72
|
+
const warned = new Set();
|
|
73
|
+
|
|
74
|
+
export function noteDeprecatedId(kind, from, to) {
|
|
75
|
+
const key = `${kind}\0${from}\0${to}`;
|
|
76
|
+
if (warned.has(key)) return;
|
|
77
|
+
warned.add(key);
|
|
78
|
+
if (process.env.KXM_QUIET_ALIASES === "1") return;
|
|
79
|
+
console.warn(`kxm: deprecated ${kind} id '${from}' resolves to '${to}'`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resetDeprecatedIdWarnings() {
|
|
83
|
+
warned.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveRenamedId(requested, knownIds, pairs) {
|
|
87
|
+
if (typeof requested !== "string" || knownIds.has(requested)) return requested;
|
|
88
|
+
for (const [oldId, newId] of pairs) {
|
|
89
|
+
if (requested === oldId && knownIds.has(newId)) return newId;
|
|
90
|
+
if (requested === newId && knownIds.has(oldId)) return oldId;
|
|
91
|
+
}
|
|
92
|
+
return requested;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function canonicalRouteId(harness, model) {
|
|
96
|
+
if (typeof harness !== "string" || typeof model !== "string" || !harness || !model) return undefined;
|
|
97
|
+
const parts = model.split("/").filter((part) => part.length > 0);
|
|
98
|
+
if (parts.length === 0) return undefined;
|
|
99
|
+
const slug = parts[parts.length - 1].replaceAll(".", "-").toLowerCase();
|
|
100
|
+
const provider = parts.length > 1 ? parts[0].replaceAll(".", "-").toLowerCase() : undefined;
|
|
101
|
+
const id = provider ? `${harness}-${slug}-${provider}` : `${harness}-${slug}`;
|
|
102
|
+
return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(id) && id.length <= 64 ? id : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function aliasList(value) {
|
|
106
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {Iterable<{ id?: string, aliases?: readonly string[] }>} records
|
|
111
|
+
* @param {string} requested
|
|
112
|
+
* @param {"route"|"agent"|"workflow"|"step"} kind
|
|
113
|
+
*/
|
|
114
|
+
export function lookupById(records, requested, kind) {
|
|
115
|
+
const list = [...records].filter((record) => typeof record?.id === "string" && record.id.length > 0);
|
|
116
|
+
const known = new Set(list.map((record) => record.id));
|
|
117
|
+
const direct = list.find((record) => record.id === requested);
|
|
118
|
+
if (direct) return { record: direct, viaAlias: false };
|
|
119
|
+
for (const record of list) {
|
|
120
|
+
if (aliasList(record.aliases).includes(requested)) {
|
|
121
|
+
noteDeprecatedId(kind, requested, record.id);
|
|
122
|
+
return { record, viaAlias: true };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const renamed = resolveRenamedId(requested, known, RENAMES[kind] ?? []);
|
|
126
|
+
if (renamed !== requested) {
|
|
127
|
+
const record = list.find((item) => item.id === renamed);
|
|
128
|
+
if (record) {
|
|
129
|
+
noteDeprecatedId(kind, requested, renamed);
|
|
130
|
+
return { record, viaAlias: true };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function findYamlBasename(directory, requested, kind) {
|
|
137
|
+
if (typeof requested !== "string" || !requested) return undefined;
|
|
138
|
+
if (existsSync(join(directory, `${requested}.yaml`))) return requested;
|
|
139
|
+
if (!existsSync(directory)) return undefined;
|
|
140
|
+
const found = [];
|
|
141
|
+
for (const name of readdirSync(directory)) {
|
|
142
|
+
if (!name.endsWith(".yaml") || name === "inventory.yaml") continue;
|
|
143
|
+
const id = name.slice(0, -5);
|
|
144
|
+
let doc;
|
|
145
|
+
try {
|
|
146
|
+
doc = YAML.parse(readFileSync(join(directory, name), "utf8"));
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const aliases = doc && typeof doc === "object" ? aliasList(doc.aliases) : [];
|
|
151
|
+
found.push({ id, aliases });
|
|
152
|
+
}
|
|
153
|
+
const hit = lookupById(found, requested, kind);
|
|
154
|
+
return hit?.record.id;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readYamlDir(directory, skipInventory) {
|
|
158
|
+
if (!existsSync(directory)) return [];
|
|
159
|
+
const docs = [];
|
|
160
|
+
for (const name of readdirSync(directory)) {
|
|
161
|
+
if (!name.endsWith(".yaml")) continue;
|
|
162
|
+
const id = name.slice(0, -5);
|
|
163
|
+
if (skipInventory && id === "inventory") continue;
|
|
164
|
+
let doc;
|
|
165
|
+
try {
|
|
166
|
+
doc = YAML.parse(readFileSync(join(directory, name), "utf8"));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
docs.push({ id, file: `${directory}/${name}`, error: error instanceof Error ? error.message : "invalid YAML" });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
172
|
+
docs.push({ id, file: `${directory}/${name}`, error: "root must be a mapping" });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
docs.push({ id, file: name, doc });
|
|
176
|
+
}
|
|
177
|
+
return docs;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function push(bucket, severity, code, file, message) {
|
|
181
|
+
bucket.push({ severity, code, file, message });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function selectorsFor(doc) {
|
|
185
|
+
const model = typeof doc.model === "string" ? doc.model : "";
|
|
186
|
+
const vendor = typeof doc.vendor === "string" ? doc.vendor : "";
|
|
187
|
+
const harness = typeof doc.harness === "string" ? doc.harness : "";
|
|
188
|
+
return [model, vendor && model ? `${vendor}/${model}` : "", harness && model ? `${harness}/${model}` : ""].filter(Boolean);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Lint one project's `.kxm` workforce. Errors fail CI. An admitted model with
|
|
193
|
+
* no route is a warning. Alias declarations are not uses; a roster or step
|
|
194
|
+
* that still cites an old id is a `deprecated_id` warning.
|
|
195
|
+
*/
|
|
196
|
+
export function lintWorkforce(root) {
|
|
197
|
+
const errors = [];
|
|
198
|
+
const warnings = [];
|
|
199
|
+
const models = readYamlDir(join(root, ".kxm", "models"), true).filter((entry) => !entry.error);
|
|
200
|
+
for (const entry of readYamlDir(join(root, ".kxm", "models"), true)) {
|
|
201
|
+
if (entry.error) push(errors, "error", "invalid_yaml", `.kxm/models/${entry.id}.yaml`, entry.error);
|
|
202
|
+
}
|
|
203
|
+
const roles = readYamlDir(join(root, ".kxm", "roles"), false).filter((entry) => !entry.error);
|
|
204
|
+
const agents = readYamlDir(join(root, ".kxm", "agents"), false).filter((entry) => !entry.error);
|
|
205
|
+
const workflows = readYamlDir(join(root, ".kxm", "workflows"), false).filter((entry) => !entry.error);
|
|
206
|
+
let admitted = [];
|
|
207
|
+
const routesFile = join(root, ".kxm", "routes.yaml");
|
|
208
|
+
if (existsSync(routesFile)) {
|
|
209
|
+
try {
|
|
210
|
+
const policy = YAML.parse(readFileSync(routesFile, "utf8"));
|
|
211
|
+
admitted = Array.isArray(policy?.admitted) ? policy.admitted.filter((item) => typeof item === "string") : [];
|
|
212
|
+
} catch (error) {
|
|
213
|
+
push(errors, "error", "invalid_yaml", ".kxm/routes.yaml", error instanceof Error ? error.message : "invalid YAML");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const modelRecords = models.map((entry) => ({ id: entry.id, aliases: aliasList(entry.doc.aliases), entry }));
|
|
218
|
+
const used = new Set();
|
|
219
|
+
for (const role of roles) {
|
|
220
|
+
const purpose = role.doc.purpose;
|
|
221
|
+
if (typeof purpose === "string" && role.id !== purpose) {
|
|
222
|
+
push(errors, "error", "role_id_convention", `.kxm/roles/${role.id}.yaml`, `role id '${role.id}' must equal purpose '${purpose}'`);
|
|
223
|
+
}
|
|
224
|
+
const roster = Array.isArray(role.doc.roster) ? role.doc.roster : [];
|
|
225
|
+
for (const item of roster) {
|
|
226
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
227
|
+
const route = item.route;
|
|
228
|
+
const effort = item.effort;
|
|
229
|
+
const routeLabel = typeof route === "string" && route ? route : "(missing route)";
|
|
230
|
+
if (typeof effort !== "string" || !ROSTER_EFFORTS.has(effort)) {
|
|
231
|
+
const detail = typeof effort === "string"
|
|
232
|
+
? `effort '${effort}' is not one of off, minimal, low, medium, high, xhigh, max`
|
|
233
|
+
: "has no effort";
|
|
234
|
+
push(errors, "error", "roster_effort_required", `.kxm/roles/${role.id}.yaml`, `roster route '${routeLabel}' ${detail}`);
|
|
235
|
+
}
|
|
236
|
+
if (typeof route !== "string" || !route) continue;
|
|
237
|
+
const found = lookupById(modelRecords, route, "route");
|
|
238
|
+
if (!found) {
|
|
239
|
+
push(errors, "error", "route_unknown", `.kxm/roles/${role.id}.yaml`, `roster route '${route}' does not match a model file`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
used.add(found.record.id);
|
|
243
|
+
if (found.viaAlias) {
|
|
244
|
+
push(warnings, "warning", "deprecated_id", `.kxm/roles/${role.id}.yaml`, `roster route '${route}' is a deprecated alias of '${found.record.id}'`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const covered = new Set();
|
|
250
|
+
for (const model of models) {
|
|
251
|
+
const doc = model.doc;
|
|
252
|
+
if (typeof doc.id === "string" && doc.id !== model.id) {
|
|
253
|
+
push(errors, "error", "route_id_convention", `.kxm/models/${model.id}.yaml`, `declared id '${doc.id}' does not match the filename`);
|
|
254
|
+
}
|
|
255
|
+
const expected = canonicalRouteId(doc.harness, doc.model);
|
|
256
|
+
if (!expected || model.id !== expected) {
|
|
257
|
+
push(errors, "error", "route_id_convention", `.kxm/models/${model.id}.yaml`, `route id '${model.id}' must be '${expected ?? "<harness>-<model-slug>[-<provider>]"}'`);
|
|
258
|
+
}
|
|
259
|
+
if (!used.has(model.id)) {
|
|
260
|
+
push(errors, "error", "route_unused", `.kxm/models/${model.id}.yaml`, `route '${model.id}' is not on any role roster`);
|
|
261
|
+
}
|
|
262
|
+
const selectorHit = selectorsFor(doc).find((selector) => admitted.includes(selector));
|
|
263
|
+
if (!selectorHit) {
|
|
264
|
+
push(errors, "error", "route_not_admitted", `.kxm/models/${model.id}.yaml`, `model '${doc.model ?? ""}' is not an admitted selector`);
|
|
265
|
+
} else {
|
|
266
|
+
covered.add(selectorHit);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const selector of admitted) {
|
|
270
|
+
if (!covered.has(selector)) {
|
|
271
|
+
push(warnings, "warning", "admitted_unrouted", ".kxm/routes.yaml", `admitted model '${selector}' is not used by any route`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const agentRecords = agents.map((entry) => ({ id: entry.id, aliases: aliasList(entry.doc.aliases), entry }));
|
|
276
|
+
for (const agent of agents) {
|
|
277
|
+
const role = agent.doc.role;
|
|
278
|
+
if (typeof role !== "string" || agent.id !== role) {
|
|
279
|
+
push(errors, "error", "agent_id_convention", `.kxm/agents/${agent.id}.yaml`, `agent id '${agent.id}' must equal its role '${role ?? ""}'`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const workflow of workflows) {
|
|
283
|
+
if (!RESERVED_WORKFLOW_IDS.has(workflow.id) && !ROLE_IDS.some((role) => workflow.id === `${role}-only`)) {
|
|
284
|
+
push(errors, "error", "workflow_id_convention", `.kxm/workflows/${workflow.id}.yaml`, `workflow id '${workflow.id}' must be default, land, or <role>-only`);
|
|
285
|
+
}
|
|
286
|
+
const steps = Array.isArray(workflow.doc.steps) ? workflow.doc.steps : [];
|
|
287
|
+
const realIds = new Set(steps.map((step) => step && typeof step.id === "string" ? step.id : "").filter(Boolean));
|
|
288
|
+
for (const step of steps) {
|
|
289
|
+
if (!step || typeof step !== "object" || typeof step.id !== "string") continue;
|
|
290
|
+
if (step.kind === "agent" || step.kind === "moa") {
|
|
291
|
+
const agentName = typeof step.agent === "string" ? step.agent : "";
|
|
292
|
+
const found = agentName ? lookupById(agentRecords, agentName, "agent") : undefined;
|
|
293
|
+
if (!found) {
|
|
294
|
+
push(errors, "error", "agent_unknown", `.kxm/workflows/${workflow.id}.yaml`, `step '${step.id}' references unknown agent '${agentName}'`);
|
|
295
|
+
} else if (found.viaAlias) {
|
|
296
|
+
push(warnings, "warning", "deprecated_id", `.kxm/workflows/${workflow.id}.yaml`, `step '${step.id}' agent '${agentName}' is a deprecated alias of '${found.record.id}'`);
|
|
297
|
+
}
|
|
298
|
+
const role = found?.record.entry.doc.role;
|
|
299
|
+
if (typeof role === "string" && step.id !== role) {
|
|
300
|
+
push(errors, "error", "step_id_convention", `.kxm/workflows/${workflow.id}.yaml`, `agent step '${step.id}' must use the role id '${role}'`);
|
|
301
|
+
}
|
|
302
|
+
} else if (step.kind === "gate") {
|
|
303
|
+
const gate = typeof step.gate === "string" ? step.gate : "";
|
|
304
|
+
const allowed = step.id === "verify" || step.id === gate || step.id.startsWith(`${workflow.id}-`);
|
|
305
|
+
if (!allowed) {
|
|
306
|
+
push(errors, "error", "step_id_convention", `.kxm/workflows/${workflow.id}.yaml`, `gate step '${step.id}' must be verify, the gate id, or ${workflow.id}-<name>`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const alias of aliasList(step.aliases)) {
|
|
310
|
+
if (realIds.has(alias) && alias !== step.id) {
|
|
311
|
+
push(errors, "error", "step_id_convention", `.kxm/workflows/${workflow.id}.yaml`, `step alias '${alias}' collides with another step id`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { errors, warnings };
|
|
317
|
+
}
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
"executor": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
14
14
|
"harness": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
15
15
|
"role": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
16
|
+
"aliases": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"description": "Deprecated agent ids that still resolve to this file.",
|
|
19
|
+
"items": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
20
|
+
"uniqueItems": true,
|
|
21
|
+
"maxItems": 16
|
|
22
|
+
},
|
|
16
23
|
"effort": { "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"] },
|
|
17
24
|
"skills": {
|
|
18
25
|
"type": "array",
|
|
@@ -14,6 +14,18 @@
|
|
|
14
14
|
"minLength": 1,
|
|
15
15
|
"maxLength": 64
|
|
16
16
|
},
|
|
17
|
+
"aliases": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"description": "Deprecated route ids that still resolve to this file. A deprecation warning is printed when one is used.",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"maxLength": 64
|
|
25
|
+
},
|
|
26
|
+
"uniqueItems": true,
|
|
27
|
+
"maxItems": 16
|
|
28
|
+
},
|
|
17
29
|
"harness": {
|
|
18
30
|
"type": "string",
|
|
19
31
|
"pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"schema": { "const": "kxm.workflow.v1" },
|
|
10
10
|
"description": { "type": "string", "maxLength": 4000 },
|
|
11
11
|
"coordinator": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
12
|
+
"aliases": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"description": "Deprecated workflow ids that still resolve to this file.",
|
|
15
|
+
"items": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
16
|
+
"uniqueItems": true,
|
|
17
|
+
"maxItems": 16
|
|
18
|
+
},
|
|
12
19
|
"limits": { "$ref": "#/$defs/workflowLimits" },
|
|
13
20
|
"reproOracle": { "$ref": "#/$defs/oracle" },
|
|
14
21
|
"planHash": { "$ref": "#/$defs/oracle" },
|
|
@@ -170,6 +177,13 @@
|
|
|
170
177
|
"required": ["id", "kind"],
|
|
171
178
|
"properties": {
|
|
172
179
|
"id": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
180
|
+
"aliases": {
|
|
181
|
+
"type": "array",
|
|
182
|
+
"description": "Deprecated step ids that still resolve to this step.",
|
|
183
|
+
"items": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
184
|
+
"uniqueItems": true,
|
|
185
|
+
"maxItems": 16
|
|
186
|
+
},
|
|
173
187
|
"kind": { "enum": ["agent", "moa", "gate", "approval", "wait"] },
|
|
174
188
|
"description": { "type": "string", "maxLength": 2000 },
|
|
175
189
|
"maxAttempts": { "type": "integer", "minimum": 1, "maximum": 20, "default": 1 },
|
package/scripts/harness-run.mjs
CHANGED
|
@@ -153,7 +153,7 @@ export const ROUTES = Object.freeze({
|
|
|
153
153
|
provider: "anthropic",
|
|
154
154
|
roles: Object.freeze(["planner", "reviewer-arch"]),
|
|
155
155
|
permissions: Object.freeze(["read-only"]),
|
|
156
|
-
models: Object.freeze(["fable"
|
|
156
|
+
models: Object.freeze(["fable"]),
|
|
157
157
|
efforts: Object.freeze(["low", "medium", "high", "xhigh", "max"]),
|
|
158
158
|
auth: Object.freeze({ args: ["auth", "status"], loginHint: "claude auth login" }),
|
|
159
159
|
},
|
|
@@ -8,12 +8,12 @@ import { buildArgv } from "./harness-run.mjs";
|
|
|
8
8
|
|
|
9
9
|
// Launch intent only: these labels are not observed model identity or admission.
|
|
10
10
|
export function nativeCriticLaunch(harness) {
|
|
11
|
-
if (!["fable", "astra"
|
|
12
|
-
throw new Error("usage: native-critic.mjs <fable|astra
|
|
11
|
+
if (!["fable", "astra"].includes(harness)) {
|
|
12
|
+
throw new Error("usage: native-critic.mjs <fable|astra> <prompt>");
|
|
13
13
|
}
|
|
14
|
-
const model = harness === "fable" ? "fable" :
|
|
15
|
-
const command = harness === "fable"
|
|
16
|
-
const args = harness === "fable"
|
|
14
|
+
const model = harness === "fable" ? "fable" : "gpt-6-astra";
|
|
15
|
+
const command = harness === "fable" ? "claude" : "codex";
|
|
16
|
+
const args = harness === "fable"
|
|
17
17
|
? ["-p", "--model", model, "--effort", "medium", "--output-format", "json", "--no-session-persistence", "--tools", "Read,Glob,Grep", "--setting-sources", "user"]
|
|
18
18
|
: buildArgv({ harness: "codex", model, effort: "low", permission: "read-only", prompt_file: "-" });
|
|
19
19
|
return { model, command, args };
|
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import YAML from 'yaml';
|
|
8
8
|
import { NATIVE_PI_BRAKE_PROVIDERS, PI_ALLOWED_PROVIDERS, PI_ANTIGRAVITY_MODEL_ID, PI_NATIVE_VENDOR_PROVIDERS, ROUTES } from './harness-run.mjs';
|
|
9
|
+
import { lookupById } from '../plugins/kxm/src/workforce-names.mjs';
|
|
9
10
|
|
|
10
11
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
12
|
const MODEL_DIR = '.kxm/models';
|
|
@@ -201,12 +202,17 @@ export function assembleRosterPolicy(models, roles) {
|
|
|
201
202
|
text(role.id, 'role id');
|
|
202
203
|
if (!Array.isArray(role.roster)) refuse('lineup contains unsupported or duplicate values');
|
|
203
204
|
const ids = [];
|
|
205
|
+
const modelRecords = models
|
|
206
|
+
.filter((doc) => doc && typeof doc.id === 'string')
|
|
207
|
+
.map((doc) => ({ id: doc.id, aliases: Array.isArray(doc.aliases) ? doc.aliases : [] }));
|
|
204
208
|
for (const entry of role.roster) {
|
|
205
209
|
record(entry, 'roster entry');
|
|
206
210
|
text(entry.route, 'roster route');
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
const resolved = lookupById(modelRecords, entry.route, 'route');
|
|
212
|
+
const routeId = resolved?.record.id ?? entry.route;
|
|
213
|
+
ids.push(routeId);
|
|
214
|
+
if (!rolesByRoute[routeId]) rolesByRoute[routeId] = [];
|
|
215
|
+
if (!rolesByRoute[routeId].includes(role.id)) rolesByRoute[routeId].push(role.id);
|
|
210
216
|
}
|
|
211
217
|
lineup[role.id] = ids;
|
|
212
218
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fail when this project's workforce ids drift: a route at a non-admitted
|
|
3
|
+
// model, a roster entry with no route file, a route no role uses, or an id
|
|
4
|
+
// that breaks the workforce convention. An admitted model with no route is a
|
|
5
|
+
// warning and does not fail the process.
|
|
6
|
+
|
|
7
|
+
import { lintWorkforce } from "../plugins/kxm/src/workforce-names.mjs";
|
|
8
|
+
|
|
9
|
+
const root = process.argv[2] ?? process.cwd();
|
|
10
|
+
const report = lintWorkforce(root);
|
|
11
|
+
for (const item of [...report.errors, ...report.warnings]) {
|
|
12
|
+
process.stderr.write(`${item.severity} ${item.file}: ${item.code}: ${item.message}\n`);
|
|
13
|
+
}
|
|
14
|
+
if (report.errors.length > 0) {
|
|
15
|
+
process.stderr.write(`workforce lint failed: ${report.errors.length} error(s), ${report.warnings.length} warning(s)\n`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
process.stdout.write(`workforce lint passed (${report.warnings.length} warning(s))\n`);
|