@kontextmind/kxm 0.6.0 → 0.7.0
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/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +5 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +2 -0
- package/docs/agent-skills.md +118 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +2 -2
- package/docs/kxm-handbook.md +3 -3
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +102 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +19 -0
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +6875 -3672
- package/plugins/kxm/dist/core.js +214 -34
- package/plugins/kxm/dist/extension.js +7721 -85
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5403 -1014
- package/plugins/kxm/dist/server.js +3008 -2268
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +73 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +35 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- package/plugins/kxm/src/cli.ts +1077 -11
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +356 -7
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +375 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-config.ts +15 -110
- package/plugins/kxm/src/vnext-engine.ts +198 -62
- package/plugins/kxm/src/vnext-harness.ts +263 -81
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
- package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-hub.mjs +6 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- package/.kxm/config/workflows/v04-dogfood.json +0 -72
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KXM Role Management Subsystem (kxm.role.v1).
|
|
3
|
+
* Supports modular per-role YAML files with descriptions, skills, tool permissions, and model rosters.
|
|
4
|
+
* Implements global (~/.config/kxm/roles/) and local (.kxm/roles/) inheritance.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { parse, stringify } from "yaml";
|
|
10
|
+
import { repoConfigDirectory, userConfigDirectory } from "./config.ts";
|
|
11
|
+
|
|
12
|
+
export const KXM_ROLE_SCHEMA = "kxm.role.v1" as const;
|
|
13
|
+
|
|
14
|
+
export interface KxmRosterEntry {
|
|
15
|
+
harness: string;
|
|
16
|
+
model: string;
|
|
17
|
+
provider?: string | undefined;
|
|
18
|
+
effort?: "low" | "medium" | "high" | "xhigh" | undefined;
|
|
19
|
+
mode?: "headless" | "interactive" | "either" | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface KxmRoleToolPolicy {
|
|
23
|
+
preset?: string | undefined;
|
|
24
|
+
allow?: string[] | undefined;
|
|
25
|
+
deny?: string[] | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface KxmTemplateContract {
|
|
29
|
+
template: string;
|
|
30
|
+
schema?: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface KxmRolePolicy {
|
|
34
|
+
vendorIndependenceRequired?: boolean | undefined;
|
|
35
|
+
maxTransitions?: number | undefined;
|
|
36
|
+
requiresGateVerification?: boolean | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface KxmRoleDefinition {
|
|
40
|
+
schema: typeof KXM_ROLE_SCHEMA;
|
|
41
|
+
id: string;
|
|
42
|
+
description: string;
|
|
43
|
+
skills?: string[] | undefined;
|
|
44
|
+
tools?: KxmRoleToolPolicy | undefined;
|
|
45
|
+
produces?: KxmTemplateContract[] | undefined;
|
|
46
|
+
consumes?: KxmTemplateContract[] | undefined;
|
|
47
|
+
roster: KxmRosterEntry[];
|
|
48
|
+
policy?: KxmRolePolicy | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface KxmRoleSummary {
|
|
52
|
+
id: string;
|
|
53
|
+
description: string;
|
|
54
|
+
scope: "global" | "local" | "overridden";
|
|
55
|
+
filePath: string;
|
|
56
|
+
skills: string[];
|
|
57
|
+
toolsCount: number;
|
|
58
|
+
roster: KxmRosterEntry[];
|
|
59
|
+
primaryModel?: string | undefined;
|
|
60
|
+
primaryHarness?: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const DEFAULT_ROLES: Record<string, KxmRoleDefinition> = {
|
|
64
|
+
writer: {
|
|
65
|
+
schema: KXM_ROLE_SCHEMA,
|
|
66
|
+
id: "writer",
|
|
67
|
+
description: "Primary implementation agent. Writes code, refactors components, and authors unit tests.",
|
|
68
|
+
skills: ["kxm", "unit-testing"],
|
|
69
|
+
tools: {
|
|
70
|
+
preset: "author",
|
|
71
|
+
allow: ["view_file", "replace_file_content", "write_to_file", "run_command"],
|
|
72
|
+
deny: ["git push"],
|
|
73
|
+
},
|
|
74
|
+
produces: [{ template: "code-patch" }],
|
|
75
|
+
roster: [
|
|
76
|
+
{ harness: "grok", model: "grok-4.6", effort: "low", mode: "headless" },
|
|
77
|
+
{ harness: "pi", provider: "openrouter", model: "openrouter/qwen/qwen3-coder-plus", effort: "medium" },
|
|
78
|
+
{ harness: "agy", model: "gemini-2.5-pro", effort: "low" },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
planner: {
|
|
82
|
+
schema: KXM_ROLE_SCHEMA,
|
|
83
|
+
id: "planner",
|
|
84
|
+
description: "Architecture breakdown, requirement decomposition, and safety boundary definition.",
|
|
85
|
+
skills: ["kxm-session", "architecture-planning"],
|
|
86
|
+
tools: {
|
|
87
|
+
preset: "read_only",
|
|
88
|
+
allow: ["view_file", "grep_search", "find_by_name", "list_dir"],
|
|
89
|
+
deny: ["write_to_file", "replace_file_content", "run_command"],
|
|
90
|
+
},
|
|
91
|
+
produces: [{ template: "implementation-plan" }],
|
|
92
|
+
roster: [
|
|
93
|
+
{ harness: "claude", model: "fable", effort: "medium" },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
"critic-arch": {
|
|
97
|
+
schema: KXM_ROLE_SCHEMA,
|
|
98
|
+
id: "critic-arch",
|
|
99
|
+
description: "Independent critic reviewing implementation diffs for architectural integrity and permission boundaries.",
|
|
100
|
+
policy: { vendorIndependenceRequired: true },
|
|
101
|
+
tools: {
|
|
102
|
+
preset: "critic",
|
|
103
|
+
allow: ["view_file", "grep_search"],
|
|
104
|
+
},
|
|
105
|
+
produces: [{ template: "critic-signoff" }],
|
|
106
|
+
roster: [
|
|
107
|
+
{ harness: "claude", model: "fable", effort: "medium" },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
"critic-cli": {
|
|
111
|
+
schema: KXM_ROLE_SCHEMA,
|
|
112
|
+
id: "critic-cli",
|
|
113
|
+
description: "CLI, documentation, and developer ergonomics critic.",
|
|
114
|
+
policy: { vendorIndependenceRequired: true },
|
|
115
|
+
tools: {
|
|
116
|
+
preset: "critic",
|
|
117
|
+
allow: ["view_file", "grep_search"],
|
|
118
|
+
},
|
|
119
|
+
produces: [{ template: "critic-signoff" }],
|
|
120
|
+
roster: [
|
|
121
|
+
{ harness: "codex", model: "gpt-5.6-sol", effort: "low" },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
verifier: {
|
|
125
|
+
schema: KXM_ROLE_SCHEMA,
|
|
126
|
+
id: "verifier",
|
|
127
|
+
description: "Deterministic gate evaluation runner.",
|
|
128
|
+
tools: {
|
|
129
|
+
preset: "verifier",
|
|
130
|
+
allow: ["run_command"],
|
|
131
|
+
},
|
|
132
|
+
produces: [{ template: "verification-receipt" }],
|
|
133
|
+
roster: [
|
|
134
|
+
{ harness: "pi", model: "evaluator", effort: "low" },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export function rolesDirectory(
|
|
140
|
+
scope: "global" | "local",
|
|
141
|
+
repoRoot = process.cwd(),
|
|
142
|
+
userConfigDir?: string,
|
|
143
|
+
): string {
|
|
144
|
+
if (scope === "global") {
|
|
145
|
+
return join(userConfigDirectory(userConfigDir), "roles");
|
|
146
|
+
}
|
|
147
|
+
return join(repoConfigDirectory(repoRoot), "roles");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function ensureRolesDirectory(
|
|
151
|
+
scope: "global" | "local",
|
|
152
|
+
repoRoot = process.cwd(),
|
|
153
|
+
userConfigDir?: string,
|
|
154
|
+
): string {
|
|
155
|
+
const dir = rolesDirectory(scope, repoRoot, userConfigDir);
|
|
156
|
+
if (!existsSync(dir)) {
|
|
157
|
+
mkdirSync(dir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
return dir;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function ensureDefaultRoles(userConfigDir?: string): void {
|
|
163
|
+
const dir = ensureRolesDirectory("global", undefined, userConfigDir);
|
|
164
|
+
for (const [id, def] of Object.entries(DEFAULT_ROLES)) {
|
|
165
|
+
const filePath = join(dir, `${id}.yaml`);
|
|
166
|
+
if (!existsSync(filePath)) {
|
|
167
|
+
writeFileSync(filePath, stringify(def), "utf8");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function parseRoleFile(filePath: string): KxmRoleDefinition | undefined {
|
|
173
|
+
if (!existsSync(filePath)) return undefined;
|
|
174
|
+
try {
|
|
175
|
+
const raw = readFileSync(filePath, "utf8");
|
|
176
|
+
const parsed = parse(raw) as Partial<KxmRoleDefinition>;
|
|
177
|
+
if (!parsed || typeof parsed !== "object" || parsed.schema !== KXM_ROLE_SCHEMA) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const id = parsed.id || filePath.replace(/^.*[\\/]/, "").replace(/\.yaml$/i, "");
|
|
181
|
+
return {
|
|
182
|
+
schema: KXM_ROLE_SCHEMA,
|
|
183
|
+
id,
|
|
184
|
+
description: parsed.description || "",
|
|
185
|
+
skills: Array.isArray(parsed.skills) ? parsed.skills : [],
|
|
186
|
+
tools: parsed.tools,
|
|
187
|
+
produces: parsed.produces,
|
|
188
|
+
consumes: parsed.consumes,
|
|
189
|
+
roster: Array.isArray(parsed.roster) ? parsed.roster : [],
|
|
190
|
+
policy: parsed.policy,
|
|
191
|
+
};
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function listRoles(options: {
|
|
198
|
+
scope?: "all" | "global" | "local" | undefined;
|
|
199
|
+
repoRoot?: string | undefined;
|
|
200
|
+
userConfigDir?: string | undefined;
|
|
201
|
+
} = {}): KxmRoleSummary[] {
|
|
202
|
+
const scopeFilter = options.scope ?? "all";
|
|
203
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
204
|
+
const globalDir = rolesDirectory("global", repoRoot, options.userConfigDir);
|
|
205
|
+
const localDir = rolesDirectory("local", repoRoot, options.userConfigDir);
|
|
206
|
+
|
|
207
|
+
const localRoles = new Map<string, { role: KxmRoleDefinition; filePath: string }>();
|
|
208
|
+
if (scopeFilter !== "global" && existsSync(localDir)) {
|
|
209
|
+
for (const entry of readdirSync(localDir)) {
|
|
210
|
+
if (entry.endsWith(".yaml") || entry.endsWith(".yml")) {
|
|
211
|
+
const filePath = join(localDir, entry);
|
|
212
|
+
const parsed = parseRoleFile(filePath);
|
|
213
|
+
if (parsed) {
|
|
214
|
+
localRoles.set(parsed.id, { role: parsed, filePath });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const globalRoles = new Map<string, { role: KxmRoleDefinition; filePath: string }>();
|
|
221
|
+
if (scopeFilter !== "local" && existsSync(globalDir)) {
|
|
222
|
+
for (const entry of readdirSync(globalDir)) {
|
|
223
|
+
if (entry.endsWith(".yaml") || entry.endsWith(".yml")) {
|
|
224
|
+
const filePath = join(globalDir, entry);
|
|
225
|
+
const parsed = parseRoleFile(filePath);
|
|
226
|
+
if (parsed) {
|
|
227
|
+
globalRoles.set(parsed.id, { role: parsed, filePath });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result: KxmRoleSummary[] = [];
|
|
234
|
+
|
|
235
|
+
// 1. Process local roles
|
|
236
|
+
for (const [id, { role, filePath }] of localRoles) {
|
|
237
|
+
const isOverridden = globalRoles.has(id);
|
|
238
|
+
const primary = role.roster[0];
|
|
239
|
+
result.push({
|
|
240
|
+
id,
|
|
241
|
+
description: role.description,
|
|
242
|
+
scope: isOverridden ? "overridden" : "local",
|
|
243
|
+
filePath,
|
|
244
|
+
skills: role.skills ?? [],
|
|
245
|
+
toolsCount: (role.tools?.allow?.length ?? 0),
|
|
246
|
+
roster: role.roster,
|
|
247
|
+
primaryModel: primary?.model,
|
|
248
|
+
primaryHarness: primary?.harness,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 2. Process global roles not in local (or if scope is global only)
|
|
253
|
+
for (const [id, { role, filePath }] of globalRoles) {
|
|
254
|
+
if (scopeFilter === "global" || !localRoles.has(id)) {
|
|
255
|
+
const primary = role.roster[0];
|
|
256
|
+
result.push({
|
|
257
|
+
id,
|
|
258
|
+
description: role.description,
|
|
259
|
+
scope: "global",
|
|
260
|
+
filePath,
|
|
261
|
+
skills: role.skills ?? [],
|
|
262
|
+
toolsCount: (role.tools?.allow?.length ?? 0),
|
|
263
|
+
roster: role.roster,
|
|
264
|
+
primaryModel: primary?.model,
|
|
265
|
+
primaryHarness: primary?.harness,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
result.sort((a, b) => a.id.localeCompare(b.id));
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function getRole(
|
|
275
|
+
roleId: string,
|
|
276
|
+
options: {
|
|
277
|
+
scope?: "all" | "global" | "local" | undefined;
|
|
278
|
+
repoRoot?: string | undefined;
|
|
279
|
+
userConfigDir?: string | undefined;
|
|
280
|
+
} = {},
|
|
281
|
+
): { role: KxmRoleDefinition; scope: "global" | "local"; filePath: string } | undefined {
|
|
282
|
+
const scope = options.scope ?? "all";
|
|
283
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
284
|
+
|
|
285
|
+
// Check local first if allowed
|
|
286
|
+
if (scope !== "global") {
|
|
287
|
+
const localDir = rolesDirectory("local", repoRoot, options.userConfigDir);
|
|
288
|
+
const localFile = join(localDir, `${roleId}.yaml`);
|
|
289
|
+
const parsed = parseRoleFile(localFile);
|
|
290
|
+
if (parsed) return { role: parsed, scope: "local", filePath: localFile };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check global
|
|
294
|
+
if (scope !== "local") {
|
|
295
|
+
const globalDir = rolesDirectory("global", repoRoot, options.userConfigDir);
|
|
296
|
+
const globalFile = join(globalDir, `${roleId}.yaml`);
|
|
297
|
+
const parsed = parseRoleFile(globalFile);
|
|
298
|
+
if (parsed) return { role: parsed, scope: "global", filePath: globalFile };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function addRole(
|
|
305
|
+
role: KxmRoleDefinition,
|
|
306
|
+
options: {
|
|
307
|
+
scope?: "global" | "local" | undefined;
|
|
308
|
+
repoRoot?: string | undefined;
|
|
309
|
+
userConfigDir?: string | undefined;
|
|
310
|
+
overwrite?: boolean | undefined;
|
|
311
|
+
} = {},
|
|
312
|
+
): { id: string; filePath: string; scope: "global" | "local" } {
|
|
313
|
+
const scope = options.scope ?? "local";
|
|
314
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
315
|
+
const dir = ensureRolesDirectory(scope, repoRoot, options.userConfigDir);
|
|
316
|
+
const filePath = join(dir, `${role.id}.yaml`);
|
|
317
|
+
|
|
318
|
+
if (existsSync(filePath) && !options.overwrite) {
|
|
319
|
+
throw new Error(`role_already_exists: role '${role.id}' already exists at ${filePath}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
writeFileSync(filePath, stringify(role), "utf8");
|
|
323
|
+
return { id: role.id, filePath, scope };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function removeRole(
|
|
327
|
+
roleId: string,
|
|
328
|
+
options: {
|
|
329
|
+
scope?: "global" | "local" | undefined;
|
|
330
|
+
repoRoot?: string | undefined;
|
|
331
|
+
userConfigDir?: string | undefined;
|
|
332
|
+
} = {},
|
|
333
|
+
): { id: string; removed: boolean; filePath: string; scope: "global" | "local" } {
|
|
334
|
+
const scope = options.scope ?? "local";
|
|
335
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
336
|
+
const dir = rolesDirectory(scope, repoRoot, options.userConfigDir);
|
|
337
|
+
const filePath = join(dir, `${roleId}.yaml`);
|
|
338
|
+
|
|
339
|
+
if (!existsSync(filePath)) {
|
|
340
|
+
throw new Error(`role_not_found: role '${roleId}' not found in ${scope} directory (${filePath})`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
rmSync(filePath);
|
|
344
|
+
return { id: roleId, removed: true, filePath, scope };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function modifyRole(
|
|
348
|
+
roleId: string,
|
|
349
|
+
updates: Partial<KxmRoleDefinition>,
|
|
350
|
+
options: {
|
|
351
|
+
scope?: "global" | "local" | undefined;
|
|
352
|
+
repoRoot?: string | undefined;
|
|
353
|
+
userConfigDir?: string | undefined;
|
|
354
|
+
} = {},
|
|
355
|
+
): { id: string; role: KxmRoleDefinition; filePath: string; scope: "global" | "local" } {
|
|
356
|
+
const target = getRole(roleId, { scope: options.scope, repoRoot: options.repoRoot, userConfigDir: options.userConfigDir });
|
|
357
|
+
if (!target) {
|
|
358
|
+
throw new Error(`role_not_found: role '${roleId}' does not exist`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const updated: KxmRoleDefinition = {
|
|
362
|
+
...target.role,
|
|
363
|
+
...updates,
|
|
364
|
+
schema: KXM_ROLE_SCHEMA,
|
|
365
|
+
id: roleId, // preserve identity
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const scope = options.scope ?? target.scope;
|
|
369
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
370
|
+
const dir = ensureRolesDirectory(scope, repoRoot, options.userConfigDir);
|
|
371
|
+
const filePath = join(dir, `${roleId}.yaml`);
|
|
372
|
+
|
|
373
|
+
writeFileSync(filePath, stringify(updated), "utf8");
|
|
374
|
+
return { id: roleId, role: updated, filePath, scope };
|
|
375
|
+
}
|
|
@@ -532,9 +532,106 @@ export interface RoutingReport {
|
|
|
532
532
|
export interface GenerateRoutingReportOptions {
|
|
533
533
|
catalog?: PriceCatalog | undefined;
|
|
534
534
|
includeEquivalentListCost?: boolean | undefined;
|
|
535
|
+
halfLifeDays?: number | undefined;
|
|
535
536
|
now?: () => string;
|
|
536
537
|
}
|
|
537
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Calculates exponential decay weight for historical telemetry (Decision Q14).
|
|
541
|
+
* Weight w = 2^(-delta_t / halfLifeDays).
|
|
542
|
+
*/
|
|
543
|
+
export function computeDecayedWeight(
|
|
544
|
+
recordedAt: string | number,
|
|
545
|
+
halfLifeDays: number = 14,
|
|
546
|
+
now: number = Date.now(),
|
|
547
|
+
): number {
|
|
548
|
+
const ts = typeof recordedAt === "number" ? recordedAt : Date.parse(recordedAt);
|
|
549
|
+
if (!Number.isFinite(ts)) return 1.0;
|
|
550
|
+
const deltaMs = Math.max(0, now - ts);
|
|
551
|
+
const halfLifeMs = halfLifeDays * 24 * 60 * 60 * 1000;
|
|
552
|
+
return Math.pow(2, -deltaMs / halfLifeMs);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export interface RouteCircuitStatus {
|
|
556
|
+
status: "healthy" | "demoted" | "quarantined";
|
|
557
|
+
consecutiveFailures: number;
|
|
558
|
+
penaltyMultiplier: number;
|
|
559
|
+
reason?: string | undefined;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Evaluates route health against the circuit breaker policy (Decision Q13).
|
|
564
|
+
* Detects failure streaks in the observation window and applies soft demotion or quarantine.
|
|
565
|
+
*/
|
|
566
|
+
export function evaluateCircuitBreaker(
|
|
567
|
+
records: Array<RoutingRecord | RoutingRecordV2>,
|
|
568
|
+
route: { harness: string; model: string },
|
|
569
|
+
config?: {
|
|
570
|
+
mode?: "soft_demotion" | "quarantine" | undefined;
|
|
571
|
+
failureThreshold?: number | undefined;
|
|
572
|
+
windowSeconds?: number | undefined;
|
|
573
|
+
penaltyMultiplier?: number | undefined;
|
|
574
|
+
},
|
|
575
|
+
now: number = Date.now(),
|
|
576
|
+
): RouteCircuitStatus {
|
|
577
|
+
const mode = config?.mode ?? "soft_demotion";
|
|
578
|
+
const failureThreshold = config?.failureThreshold ?? 3;
|
|
579
|
+
const windowSeconds = config?.windowSeconds ?? 3600;
|
|
580
|
+
const penaltyMultiplier = config?.penaltyMultiplier ?? 5.0;
|
|
581
|
+
const windowMs = windowSeconds * 1000;
|
|
582
|
+
|
|
583
|
+
const matching = records.filter((r) => {
|
|
584
|
+
const isV2 = r.schema === ROUTING_RECORD_V2_SCHEMA;
|
|
585
|
+
const harness = isV2 ? (r as RoutingRecordV2).harness : (r.providerMetadata?.harness as string | undefined);
|
|
586
|
+
const model = isV2
|
|
587
|
+
? ((r as RoutingRecordV2).effectiveModel || (r as RoutingRecordV2).requestedModel)
|
|
588
|
+
: (r.effectiveModel || r.requestedModel);
|
|
589
|
+
if (harness !== route.harness || model !== route.model) return false;
|
|
590
|
+
|
|
591
|
+
const recAt = (r as any).recordedAt;
|
|
592
|
+
const ts = typeof recAt === "string" ? Date.parse(recAt) : 0;
|
|
593
|
+
return ts >= (now - windowMs);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
matching.sort((a, b) => {
|
|
597
|
+
const ta = typeof (a as any).recordedAt === "string" ? Date.parse((a as any).recordedAt) : 0;
|
|
598
|
+
const tb = typeof (b as any).recordedAt === "string" ? Date.parse((b as any).recordedAt) : 0;
|
|
599
|
+
return tb - ta;
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
let consecutiveFailures = 0;
|
|
603
|
+
for (const r of matching) {
|
|
604
|
+
const passed = r.verifierOutcome === "passed" || r.finalOutcome === "accepted" || r.finalOutcome === "completed";
|
|
605
|
+
if (passed && (r.retries ?? 0) === 0) {
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
consecutiveFailures++;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (consecutiveFailures >= failureThreshold) {
|
|
612
|
+
if (mode === "quarantine") {
|
|
613
|
+
return {
|
|
614
|
+
status: "quarantined",
|
|
615
|
+
consecutiveFailures,
|
|
616
|
+
penaltyMultiplier,
|
|
617
|
+
reason: `Route quarantined after ${consecutiveFailures} consecutive failures within ${windowSeconds}s`,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
status: "demoted",
|
|
622
|
+
consecutiveFailures,
|
|
623
|
+
penaltyMultiplier,
|
|
624
|
+
reason: `Route demoted with ${penaltyMultiplier}x penalty after ${consecutiveFailures} consecutive failures`,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
status: "healthy",
|
|
630
|
+
consecutiveFailures,
|
|
631
|
+
penaltyMultiplier: 1.0,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
538
635
|
export function generateRoutingReport(
|
|
539
636
|
records: Array<RoutingRecord | RoutingRecordV2>,
|
|
540
637
|
options: GenerateRoutingReportOptions = {},
|
|
@@ -675,7 +772,8 @@ export function generateRoutingReport(
|
|
|
675
772
|
tokensIn: r.tokensIn ?? null,
|
|
676
773
|
tokensOut: r.tokensOut ?? null,
|
|
677
774
|
cacheReadTokens: r.cacheReadTokens ?? null,
|
|
678
|
-
|
|
775
|
+
cacheWriteTokens: v2?.cacheWriteTokens ?? null,
|
|
776
|
+
contextTokens: v2?.contextTokens ?? null,
|
|
679
777
|
});
|
|
680
778
|
if (calc) {
|
|
681
779
|
equivTotal += calc.costUsd;
|
|
@@ -553,13 +553,20 @@ export function sessionBriefPickerEnabled(input: { env?: NodeJS.ProcessEnv; mode
|
|
|
553
553
|
return reason === "startup" || reason === "new" || reason === "fork";
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
export const KXM_SLASH_SUBCOMMANDS = ["status", "hub", "memory", "help"] as const;
|
|
556
|
+
export const KXM_SLASH_SUBCOMMANDS = ["status", "hub", "memory", "progress", "workflow", "help"] as const;
|
|
557
557
|
export type KxmSlashCommand = "brief" | typeof KXM_SLASH_SUBCOMMANDS[number];
|
|
558
558
|
|
|
559
559
|
export function parseKxmSlashArgs(args: string | undefined): KxmSlashCommand {
|
|
560
560
|
const raw = String(args ?? "").trim().toLowerCase();
|
|
561
561
|
if (raw === "" || raw === "brief") return "brief";
|
|
562
|
-
if (
|
|
562
|
+
if (
|
|
563
|
+
raw === "status"
|
|
564
|
+
|| raw === "hub"
|
|
565
|
+
|| raw === "memory"
|
|
566
|
+
|| raw === "progress"
|
|
567
|
+
|| raw === "workflow"
|
|
568
|
+
|| raw === "help"
|
|
569
|
+
) return raw;
|
|
563
570
|
return "help";
|
|
564
571
|
}
|
|
565
572
|
|