@kontextmind/kxm 0.6.0 → 0.7.10
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 +7 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +5 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
- package/docs/agent-skills.md +135 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/browser-automation.md +116 -0
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +21 -0
- package/docs/kb/how-credentials-retrieved-safely.md +31 -0
- package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
- package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
- package/docs/kb/how-to-resume-after-mfa.md +28 -0
- package/docs/kb/how-to-take-over-session.md +32 -0
- package/docs/kb/why-authentication-disappeared.md +32 -0
- package/docs/kb/why-automation-opened-different-browser.md +32 -0
- package/docs/kb/why-session-viewer-cannot-control.md +31 -0
- package/docs/kxm-handbook.md +3 -3
- package/docs/operations.md +24 -0
- package/docs/operator-pi-packages.md +63 -0
- package/docs/prompts/browser-annotate-feedback.md +41 -0
- package/docs/prompts/browser-diagnose-recover.md +38 -0
- package/docs/prompts/browser-explore.md +42 -0
- package/docs/prompts/browser-repro-fix.md +48 -0
- package/docs/prompts/browser-start.md +41 -0
- package/docs/prompts/browser-takeover.md +50 -0
- package/docs/skills/repo-work-delivery.md +107 -0
- package/docs/skills.md +2 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +41 -1
- 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 +41620 -35578
- package/plugins/kxm/dist/core.js +271 -34
- package/plugins/kxm/dist/extension.js +7759 -86
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +8218 -2328
- package/plugins/kxm/dist/server.js +3125 -2260
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +103 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
- package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
- 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 +43 -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 +42 -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/browser.ts +603 -0
- package/plugins/kxm/src/cli/context-skills.ts +373 -0
- package/plugins/kxm/src/cli/hub.ts +614 -0
- package/plugins/kxm/src/cli/roles.ts +615 -0
- package/plugins/kxm/src/cli/system.ts +906 -0
- package/plugins/kxm/src/cli/tasks.ts +364 -0
- package/plugins/kxm/src/cli/types.ts +270 -0
- package/plugins/kxm/src/cli/vnext.ts +698 -0
- package/plugins/kxm/src/cli/workflows.ts +699 -0
- package/plugins/kxm/src/cli.ts +362 -2849
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +357 -8
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/modes.ts +348 -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/protocol.ts +111 -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 +710 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/runtime.ts +4 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/ssh-remote.ts +560 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/subagent-control.ts +312 -0
- 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-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +53 -111
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +214 -62
- package/plugins/kxm/src/vnext-harness.ts +336 -84
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
- package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
- package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -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/plugins/kxm/src/workflow.ts +144 -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/modes.schema.json +56 -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-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +150 -2
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -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,710 @@
|
|
|
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
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Role Seat & Host Mapping Governance (kxm.role-hosts.v1)
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
export const KXM_ROLE_HOSTS_SCHEMA = "kxm.role-hosts.v1" as const;
|
|
382
|
+
|
|
383
|
+
export interface RoleSeatDefinition {
|
|
384
|
+
seatId: string;
|
|
385
|
+
description?: string | undefined;
|
|
386
|
+
defaultModel?: string | undefined;
|
|
387
|
+
defaultHost?: string | undefined;
|
|
388
|
+
allowedTools?: readonly string[] | undefined;
|
|
389
|
+
requiredEvidenceKind?: string | undefined;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export interface RoleSeatBinding {
|
|
393
|
+
model?: string | undefined;
|
|
394
|
+
host?: string | undefined;
|
|
395
|
+
effort?: "low" | "medium" | "high" | "xhigh" | undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface RoleHostsConfig {
|
|
399
|
+
schema: typeof KXM_ROLE_HOSTS_SCHEMA;
|
|
400
|
+
seats?: Record<string, RoleSeatBinding> | undefined;
|
|
401
|
+
hostProviders?: Record<string, string> | undefined;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export const DEFAULT_ROLE_SEATS: Record<string, RoleSeatDefinition> = {
|
|
405
|
+
planner: {
|
|
406
|
+
seatId: "planner",
|
|
407
|
+
description: "Architecture breakdown, requirement decomposition, and safety boundary definition.",
|
|
408
|
+
defaultModel: "anthropic/claude-fable-5.1",
|
|
409
|
+
defaultHost: "pi",
|
|
410
|
+
allowedTools: ["view_file", "grep_search", "find_by_name", "list_dir"],
|
|
411
|
+
requiredEvidenceKind: "architecture_review",
|
|
412
|
+
},
|
|
413
|
+
writer: {
|
|
414
|
+
seatId: "writer",
|
|
415
|
+
description: "Primary implementation agent. Writes code, refactors components, and authors unit tests.",
|
|
416
|
+
defaultModel: "x-ai/grok-4.6",
|
|
417
|
+
defaultHost: "grok",
|
|
418
|
+
allowedTools: ["view_file", "replace_file_content", "write_to_file", "run_command"],
|
|
419
|
+
requiredEvidenceKind: "git_diff",
|
|
420
|
+
},
|
|
421
|
+
"critic-arch": {
|
|
422
|
+
seatId: "critic-arch",
|
|
423
|
+
description: "Independent critic reviewing implementation diffs for architectural integrity.",
|
|
424
|
+
defaultModel: "anthropic/claude-fable-5.1",
|
|
425
|
+
defaultHost: "pi",
|
|
426
|
+
allowedTools: ["view_file", "grep_search"],
|
|
427
|
+
requiredEvidenceKind: "architecture_review",
|
|
428
|
+
},
|
|
429
|
+
"critic-cli": {
|
|
430
|
+
seatId: "critic-cli",
|
|
431
|
+
description: "CLI, documentation, and developer ergonomics critic.",
|
|
432
|
+
defaultModel: "openai/gpt-5.6-sol",
|
|
433
|
+
defaultHost: "pi",
|
|
434
|
+
allowedTools: ["view_file", "grep_search"],
|
|
435
|
+
requiredEvidenceKind: "cli_review",
|
|
436
|
+
},
|
|
437
|
+
verifier: {
|
|
438
|
+
seatId: "verifier",
|
|
439
|
+
description: "Deterministic gate evaluation runner.",
|
|
440
|
+
defaultModel: "evaluator",
|
|
441
|
+
defaultHost: "pi",
|
|
442
|
+
allowedTools: ["run_command"],
|
|
443
|
+
requiredEvidenceKind: "test_run",
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export function roleHostsFilePath(
|
|
448
|
+
scope: "global" | "local",
|
|
449
|
+
repoRoot = process.cwd(),
|
|
450
|
+
userConfigDir?: string,
|
|
451
|
+
preferJson = false,
|
|
452
|
+
): string {
|
|
453
|
+
const dir = scope === "global" ? userConfigDirectory(userConfigDir) : repoConfigDirectory(repoRoot);
|
|
454
|
+
return join(dir, preferJson ? "role-hosts.json" : "role-hosts.yaml");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function findExistingRoleHostsFile(
|
|
458
|
+
scope: "global" | "local",
|
|
459
|
+
repoRoot = process.cwd(),
|
|
460
|
+
userConfigDir?: string,
|
|
461
|
+
): { filePath: string; exists: boolean; format: "yaml" | "json" } {
|
|
462
|
+
const dir = scope === "global" ? userConfigDirectory(userConfigDir) : repoConfigDirectory(repoRoot);
|
|
463
|
+
const yamlPath = join(dir, "role-hosts.yaml");
|
|
464
|
+
const ymlPath = join(dir, "role-hosts.yml");
|
|
465
|
+
const jsonPath = join(dir, "role-hosts.json");
|
|
466
|
+
|
|
467
|
+
if (existsSync(yamlPath)) return { filePath: yamlPath, exists: true, format: "yaml" };
|
|
468
|
+
if (existsSync(ymlPath)) return { filePath: ymlPath, exists: true, format: "yaml" };
|
|
469
|
+
if (existsSync(jsonPath)) return { filePath: jsonPath, exists: true, format: "json" };
|
|
470
|
+
return { filePath: yamlPath, exists: false, format: "yaml" };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function parseRoleHostsFile(filePath: string): RoleHostsConfig | undefined {
|
|
474
|
+
if (!existsSync(filePath)) return undefined;
|
|
475
|
+
try {
|
|
476
|
+
const raw = readFileSync(filePath, "utf8");
|
|
477
|
+
const parsed = parse(raw) as Partial<RoleHostsConfig>;
|
|
478
|
+
if (!parsed || typeof parsed !== "object") return undefined;
|
|
479
|
+
if (parsed.schema && parsed.schema !== KXM_ROLE_HOSTS_SCHEMA) return undefined;
|
|
480
|
+
return {
|
|
481
|
+
schema: KXM_ROLE_HOSTS_SCHEMA,
|
|
482
|
+
seats: parsed.seats && typeof parsed.seats === "object" ? parsed.seats : {},
|
|
483
|
+
hostProviders: parsed.hostProviders && typeof parsed.hostProviders === "object" ? parsed.hostProviders : {},
|
|
484
|
+
};
|
|
485
|
+
} catch {
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function loadRoleHostsConfig(options: {
|
|
491
|
+
scope?: "all" | "global" | "local" | undefined;
|
|
492
|
+
repoRoot?: string | undefined;
|
|
493
|
+
userConfigDir?: string | undefined;
|
|
494
|
+
} = {}): { config: RoleHostsConfig; filePath?: string | undefined; scope: "local" | "global" | "default" } {
|
|
495
|
+
const scopeFilter = options.scope ?? "all";
|
|
496
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
497
|
+
|
|
498
|
+
let localConfig: RoleHostsConfig | undefined;
|
|
499
|
+
let localPath: string | undefined;
|
|
500
|
+
if (scopeFilter !== "global") {
|
|
501
|
+
const localFound = findExistingRoleHostsFile("local", repoRoot, options.userConfigDir);
|
|
502
|
+
if (localFound.exists) {
|
|
503
|
+
localConfig = parseRoleHostsFile(localFound.filePath);
|
|
504
|
+
localPath = localFound.filePath;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let globalConfig: RoleHostsConfig | undefined;
|
|
509
|
+
let globalPath: string | undefined;
|
|
510
|
+
if (scopeFilter !== "local") {
|
|
511
|
+
const globalFound = findExistingRoleHostsFile("global", repoRoot, options.userConfigDir);
|
|
512
|
+
if (globalFound.exists) {
|
|
513
|
+
globalConfig = parseRoleHostsFile(globalFound.filePath);
|
|
514
|
+
globalPath = globalFound.filePath;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (scopeFilter === "local") {
|
|
519
|
+
return {
|
|
520
|
+
config: localConfig ?? { schema: KXM_ROLE_HOSTS_SCHEMA, seats: {}, hostProviders: {} },
|
|
521
|
+
filePath: localPath,
|
|
522
|
+
scope: localPath ? "local" : "default",
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (scopeFilter === "global") {
|
|
527
|
+
return {
|
|
528
|
+
config: globalConfig ?? { schema: KXM_ROLE_HOSTS_SCHEMA, seats: {}, hostProviders: {} },
|
|
529
|
+
filePath: globalPath,
|
|
530
|
+
scope: globalPath ? "global" : "default",
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Merge: local seats override global seats; local hostProviders merge over global hostProviders
|
|
535
|
+
const mergedSeats: Record<string, RoleSeatBinding> = {
|
|
536
|
+
...(globalConfig?.seats ?? {}),
|
|
537
|
+
...(localConfig?.seats ?? {}),
|
|
538
|
+
};
|
|
539
|
+
const mergedHostProviders: Record<string, string> = {
|
|
540
|
+
...(globalConfig?.hostProviders ?? {}),
|
|
541
|
+
...(localConfig?.hostProviders ?? {}),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const primaryPath = localPath ?? globalPath;
|
|
545
|
+
const primaryScope = localPath ? "local" : (globalPath ? "global" : "default");
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
config: {
|
|
549
|
+
schema: KXM_ROLE_HOSTS_SCHEMA,
|
|
550
|
+
seats: mergedSeats,
|
|
551
|
+
hostProviders: mergedHostProviders,
|
|
552
|
+
},
|
|
553
|
+
filePath: primaryPath,
|
|
554
|
+
scope: primaryScope,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function saveRoleHostsConfig(
|
|
559
|
+
config: RoleHostsConfig,
|
|
560
|
+
options: {
|
|
561
|
+
scope?: "global" | "local" | undefined;
|
|
562
|
+
repoRoot?: string | undefined;
|
|
563
|
+
userConfigDir?: string | undefined;
|
|
564
|
+
format?: "yaml" | "json" | undefined;
|
|
565
|
+
} = {},
|
|
566
|
+
): { filePath: string; scope: "global" | "local" } {
|
|
567
|
+
const scope = options.scope ?? "local";
|
|
568
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
569
|
+
const dir = scope === "global" ? userConfigDirectory(options.userConfigDir) : repoConfigDirectory(repoRoot);
|
|
570
|
+
if (!existsSync(dir)) {
|
|
571
|
+
mkdirSync(dir, { recursive: true });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const format = options.format ?? "yaml";
|
|
575
|
+
const filePath = join(dir, format === "json" ? "role-hosts.json" : "role-hosts.yaml");
|
|
576
|
+
const payload: RoleHostsConfig = {
|
|
577
|
+
schema: KXM_ROLE_HOSTS_SCHEMA,
|
|
578
|
+
seats: config.seats ?? {},
|
|
579
|
+
...(config.hostProviders && Object.keys(config.hostProviders).length > 0 ? { hostProviders: config.hostProviders } : {}),
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const content = format === "json" ? JSON.stringify(payload, null, 2) + "\n" : stringify(payload);
|
|
583
|
+
writeFileSync(filePath, content, "utf8");
|
|
584
|
+
return { filePath, scope };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function setRoleSeatHost(
|
|
588
|
+
seatId: string,
|
|
589
|
+
host: string,
|
|
590
|
+
options: {
|
|
591
|
+
model?: string | undefined;
|
|
592
|
+
effort?: "low" | "medium" | "high" | "xhigh" | undefined;
|
|
593
|
+
scope?: "global" | "local" | undefined;
|
|
594
|
+
repoRoot?: string | undefined;
|
|
595
|
+
userConfigDir?: string | undefined;
|
|
596
|
+
format?: "yaml" | "json" | undefined;
|
|
597
|
+
} = {},
|
|
598
|
+
): { filePath: string; seatId: string; binding: RoleSeatBinding; scope: "global" | "local" } {
|
|
599
|
+
const scope = options.scope ?? "local";
|
|
600
|
+
const current = loadRoleHostsConfig({
|
|
601
|
+
scope,
|
|
602
|
+
repoRoot: options.repoRoot,
|
|
603
|
+
userConfigDir: options.userConfigDir,
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const seats = { ...(current.config.seats ?? {}) };
|
|
607
|
+
const existing = seats[seatId] ?? {};
|
|
608
|
+
const binding: RoleSeatBinding = {
|
|
609
|
+
...existing,
|
|
610
|
+
host,
|
|
611
|
+
...(options.model !== undefined ? { model: options.model } : {}),
|
|
612
|
+
...(options.effort !== undefined ? { effort: options.effort } : {}),
|
|
613
|
+
};
|
|
614
|
+
seats[seatId] = binding;
|
|
615
|
+
|
|
616
|
+
const updatedConfig: RoleHostsConfig = {
|
|
617
|
+
...current.config,
|
|
618
|
+
schema: KXM_ROLE_HOSTS_SCHEMA,
|
|
619
|
+
seats,
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const saved = saveRoleHostsConfig(updatedConfig, {
|
|
623
|
+
scope,
|
|
624
|
+
repoRoot: options.repoRoot,
|
|
625
|
+
userConfigDir: options.userConfigDir,
|
|
626
|
+
format: options.format,
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
return { filePath: saved.filePath, seatId, binding, scope };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function resolveRoleSeat(
|
|
633
|
+
seatId: string,
|
|
634
|
+
options: {
|
|
635
|
+
hostOverride?: string | undefined;
|
|
636
|
+
modelOverride?: string | undefined;
|
|
637
|
+
repoRoot?: string | undefined;
|
|
638
|
+
userConfigDir?: string | undefined;
|
|
639
|
+
} = {},
|
|
640
|
+
): {
|
|
641
|
+
seatId: string;
|
|
642
|
+
host: string;
|
|
643
|
+
model?: string | undefined;
|
|
644
|
+
provider?: string | undefined;
|
|
645
|
+
effort?: "low" | "medium" | "high" | "xhigh" | undefined;
|
|
646
|
+
source: "override" | "role-hosts" | "seat-default" | "role-roster" | "fallback";
|
|
647
|
+
} {
|
|
648
|
+
const hostsConfig = loadRoleHostsConfig({
|
|
649
|
+
scope: "all",
|
|
650
|
+
repoRoot: options.repoRoot,
|
|
651
|
+
userConfigDir: options.userConfigDir,
|
|
652
|
+
}).config;
|
|
653
|
+
|
|
654
|
+
const configuredSeat = hostsConfig.seats?.[seatId];
|
|
655
|
+
const defaultSeat = DEFAULT_ROLE_SEATS[seatId];
|
|
656
|
+
const roleDef = getRole(seatId, {
|
|
657
|
+
scope: "all",
|
|
658
|
+
repoRoot: options.repoRoot,
|
|
659
|
+
userConfigDir: options.userConfigDir,
|
|
660
|
+
})?.role ?? DEFAULT_ROLES[seatId];
|
|
661
|
+
const primaryRoster = roleDef?.roster?.[0];
|
|
662
|
+
|
|
663
|
+
// Resolve host
|
|
664
|
+
let host = "pi";
|
|
665
|
+
let source: "override" | "role-hosts" | "seat-default" | "role-roster" | "fallback" = "fallback";
|
|
666
|
+
|
|
667
|
+
if (options.hostOverride) {
|
|
668
|
+
host = options.hostOverride;
|
|
669
|
+
source = "override";
|
|
670
|
+
} else if (configuredSeat?.host) {
|
|
671
|
+
host = configuredSeat.host;
|
|
672
|
+
source = "role-hosts";
|
|
673
|
+
} else if (defaultSeat?.defaultHost) {
|
|
674
|
+
host = defaultSeat.defaultHost;
|
|
675
|
+
source = "seat-default";
|
|
676
|
+
} else if (primaryRoster?.harness) {
|
|
677
|
+
host = primaryRoster.harness;
|
|
678
|
+
source = "role-roster";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Resolve model
|
|
682
|
+
let model: string | undefined;
|
|
683
|
+
if (options.modelOverride) {
|
|
684
|
+
model = options.modelOverride;
|
|
685
|
+
} else if (configuredSeat?.model) {
|
|
686
|
+
model = configuredSeat.model;
|
|
687
|
+
} else if (defaultSeat?.defaultModel) {
|
|
688
|
+
model = defaultSeat.defaultModel;
|
|
689
|
+
} else if (primaryRoster?.model) {
|
|
690
|
+
model = primaryRoster.model;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Resolve provider
|
|
694
|
+
const provider = hostsConfig.hostProviders?.[host]
|
|
695
|
+
?? roleDef?.roster?.find((r) => r.harness === host)?.provider;
|
|
696
|
+
|
|
697
|
+
// Resolve effort
|
|
698
|
+
const effort = configuredSeat?.effort
|
|
699
|
+
?? roleDef?.roster?.find((r) => r.harness === host)?.effort;
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
seatId,
|
|
703
|
+
host,
|
|
704
|
+
model,
|
|
705
|
+
provider,
|
|
706
|
+
effort,
|
|
707
|
+
source,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|