takomi 2.1.26 → 2.1.28

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.
@@ -56,7 +56,7 @@ import {
56
56
  getProfileDefaults,
57
57
  loadTakomiProfile,
58
58
  } from "./profile";
59
- import { installTakomiRoutingPolicy, resolveTakomiRoutingPolicy } from "./routing-policy";
59
+ import { installTakomiRoutingPolicy, previewTakomiRoutingPolicy, renderRoutingPolicyPreview, resolveTakomiRoutingPolicy } from "./routing-policy";
60
60
 
61
61
  type TakomiState = {
62
62
  enabled: boolean;
@@ -84,6 +84,9 @@ const STATE_ENTRY = "takomi-runtime-state";
84
84
 
85
85
  let activeProfile: TakomiProfile = DEFAULT_TAKOMI_PROFILE;
86
86
  let activeSubagentLabel: string | undefined;
87
+ let activeSubagentAgent: string | undefined;
88
+ let activeSubagentTask: string | undefined;
89
+ let activeSubagentStatus: string | undefined;
87
90
 
88
91
  const ThinkingSchema = Type.Union([
89
92
  Type.Literal("off"),
@@ -665,18 +668,27 @@ export default function takomiRuntime(pi: ExtensionAPI) {
665
668
  launchMode: state.launchMode,
666
669
  planMode: state.planMode,
667
670
  activeSubagent: activeSubagentLabel,
671
+ activeSubagentAgent,
672
+ activeSubagentTask,
673
+ activeSubagentStatus,
668
674
  });
669
675
  }
670
676
 
671
677
  async function applySubagentRuntimeEvent(event: TakomiSubagentRuntimeEvent, ctx: ExtensionContext): Promise<void> {
672
678
  if (event.type === "start") {
673
679
  activeSubagentLabel = `${event.state.agent}: ${event.state.taskLabel}`;
680
+ activeSubagentAgent = event.state.agent;
681
+ activeSubagentTask = event.state.taskLabel;
682
+ activeSubagentStatus = event.state.status ?? "running";
674
683
  syncContextPanelState();
675
684
  } else if ((event.type === "update" || event.type === "complete" || event.type === "block") && event.patch) {
676
685
  const model = event.patch.model ? ` @ ${event.patch.model}` : "";
677
686
  const thinking = event.patch.thinking ? ` (${event.patch.thinking})` : "";
678
687
  const label = event.patch.summary?.split(/\r?\n/).find(Boolean);
679
688
  if (label) activeSubagentLabel = `${label}${model}${thinking}`;
689
+ if (event.patch.agent) activeSubagentAgent = event.patch.agent;
690
+ if (event.patch.taskLabel) activeSubagentTask = event.patch.taskLabel;
691
+ activeSubagentStatus = event.type === "complete" ? "completed" : event.type === "block" ? "blocked" : event.patch.status ?? activeSubagentStatus;
680
692
  syncContextPanelState();
681
693
  }
682
694
  switch (event.type) {
@@ -761,6 +773,9 @@ export default function takomiRuntime(pi: ExtensionAPI) {
761
773
  await updateState(ctx, () => {
762
774
  state = cloneState(DEFAULT_STATE);
763
775
  activeSubagentLabel = undefined;
776
+ activeSubagentAgent = undefined;
777
+ activeSubagentTask = undefined;
778
+ activeSubagentStatus = undefined;
764
779
  }, "Takomi runtime state reset");
765
780
  subagentController.reset(ctx);
766
781
  contextPanel.resetSession();
@@ -799,6 +814,47 @@ export default function takomiRuntime(pi: ExtensionAPI) {
799
814
  },
800
815
  });
801
816
 
817
+ pi.registerTool({
818
+ name: "takomi_apply_routing_policy",
819
+ label: "Takomi Routing",
820
+ description: "Apply a Takomi model-routing policy after deterministic extraction and active-model review.",
821
+ promptSnippet: "Save reviewed Takomi model routing policy text to the active global or project policy file.",
822
+ promptGuidelines: [
823
+ "Use takomi_apply_routing_policy only after reviewing the deterministic extraction against the original routing policy text.",
824
+ "Do not call takomi_apply_routing_policy if the policy is ambiguous, invents providers, or maps to non-Takomi roles.",
825
+ ],
826
+ parameters: Type.Object({
827
+ policyText: Type.String({ description: "Original routing policy text to save" }),
828
+ scope: Type.Optional(StringEnum(["global", "project"] as const)),
829
+ reviewNotes: Type.Optional(Type.String({ description: "Brief notes from the active-model review" })),
830
+ }),
831
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
832
+ const scope = params.scope ?? "global";
833
+ const preview = previewTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
834
+ const result = await installTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
835
+ const scopeNote = scope === "global"
836
+ ? "This global policy applies unless a project-local override exists."
837
+ : "This project-local policy overrides the global policy for the current project.";
838
+ return {
839
+ content: [{
840
+ type: "text",
841
+ text: [
842
+ `Takomi routing policy saved (${scope}).`,
843
+ "",
844
+ `Policy: ${result.policyPath}`,
845
+ `Settings: ${result.settingsPath}`,
846
+ "",
847
+ renderRoutingPolicyPreview(preview),
848
+ params.reviewNotes ? `\nReview notes:\n${params.reviewNotes}` : "",
849
+ "",
850
+ scopeNote,
851
+ ].filter(Boolean).join("\n"),
852
+ }],
853
+ details: { result, preview, reviewNotes: params.reviewNotes },
854
+ };
855
+ },
856
+ });
857
+
802
858
  pi.registerTool({
803
859
  name: "takomi_workflow",
804
860
  label: "Takomi Workflow",
@@ -1067,11 +1123,28 @@ ${stateJson}`
1067
1123
  if (routingUpdateMatch) {
1068
1124
  state.enabled = true;
1069
1125
  try {
1070
- const result = await installTakomiRoutingPolicy(runtimeCtx?.cwd ?? process.cwd(), text);
1071
- const detected = result.detectedDefaults.length ? `\n\nDetected defaults:\n- ${result.detectedDefaults.join("\n- ")}` : "\n\nNo model names were auto-detected; saved policy only.";
1072
- return { action: "transform", text: `Takomi routing policy has been updated.\n\nPolicy: ${result.policyPath}\nSettings: ${result.settingsPath}${detected}\n\nAcknowledge the update briefly and explain that future Takomi turns will load this policy.` };
1126
+ const cwd = runtimeCtx?.cwd ?? process.cwd();
1127
+ const preview = previewTakomiRoutingPolicy(cwd, text, { scope: "global" });
1128
+ return { action: "transform", text: [
1129
+ "Review this Takomi routing policy extraction before it is saved.",
1130
+ "",
1131
+ "Rules:",
1132
+ "- Do not invent providers or model IDs not grounded in the policy.",
1133
+ "- Providerless names like GPT-5.5 are routing intent unless a preferred provider/router is declared.",
1134
+ "- Valid Takomi roles are: general, orchestrator, architect, designer, coder, reviewer.",
1135
+ "- If the extraction is correct and safe, call takomi_apply_routing_policy with scope=global and the exact original policy text.",
1136
+ "- If it is ambiguous or wrong, explain what the user should clarify and do not call the tool.",
1137
+ "",
1138
+ "Deterministic extraction:",
1139
+ renderRoutingPolicyPreview(preview),
1140
+ "",
1141
+ "Original policy text:",
1142
+ "```",
1143
+ preview.policy,
1144
+ "```",
1145
+ ].join("\n") };
1073
1146
  } catch (error) {
1074
- return { action: "transform", text: `Takomi routing policy update failed: ${error instanceof Error ? error.message : String(error)}` };
1147
+ return { action: "transform", text: `Takomi routing policy review failed: ${error instanceof Error ? error.message : String(error)}` };
1075
1148
  }
1076
1149
  }
1077
1150
 
@@ -1144,6 +1217,24 @@ ${stateJson}`
1144
1217
  }
1145
1218
 
1146
1219
  const routingPolicy = await resolveTakomiRoutingPolicy(runtimeCwd);
1220
+ const optionalFeatureContext = (() => {
1221
+ try {
1222
+ const tools = typeof (pi as { getAllTools?: () => Array<{ name?: string }> }).getAllTools === "function"
1223
+ ? (pi as { getAllTools: () => Array<{ name?: string }> }).getAllTools()
1224
+ : [];
1225
+ const toolNames = new Set(tools.map((tool) => tool.name).filter(Boolean));
1226
+ const guidance: string[] = [];
1227
+ if (toolNames.has("ask_user_question")) {
1228
+ guidance.push("Takomi Interview is available: when Genesis, Design, or ambiguous planning would otherwise require guessing, use ask_user_question to ask concise structured questions before proceeding.");
1229
+ }
1230
+ if (toolNames.has("todo")) {
1231
+ guidance.push("Takomi Todo is available as an optional live overlay. You may use todo for short-lived execution visibility, but takomi_board remains the durable lifecycle/task source of truth.");
1232
+ }
1233
+ return guidance.join("\n");
1234
+ } catch {
1235
+ return "";
1236
+ }
1237
+ })();
1147
1238
  const modelPreflightContext = (() => {
1148
1239
  try {
1149
1240
  const available = typeof (ctx as { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } }).modelRegistry?.getAvailable === "function"
@@ -1165,6 +1256,7 @@ ${stateJson}`
1165
1256
  routingPolicy.text
1166
1257
  ? `${routingPolicy.source === "bundled" ? "Bundled" : "Project"} Takomi model routing policy is active. Apply it when choosing parent/subagent models and escalation levels:\n\n${routingPolicy.text}`
1167
1258
  : "No Takomi routing policy file was found. Users can install one with `/takomi routing <policy>` or by saying `Update Takomi routing logic: \"\"\"...\"\"\"`.",
1259
+ optionalFeatureContext,
1168
1260
  modelPreflightContext,
1169
1261
  `Execution mode: ${route.executionMode}. Session recommendation: ${route.sessionRecommendation}.`,
1170
1262
  `Takomi execution gate: ${effectiveState.launchMode === "manual" ? "review" : "auto"}. In review gate mode, show the delegation plan before launching and return to the user after each task with results, verification guidance, and the recommended next step.`,
@@ -1199,8 +1291,10 @@ ${stateJson}`
1199
1291
  runtimeCtx = ctx;
1200
1292
  activeProfile = await loadTakomiProfile(ctx.cwd);
1201
1293
  activeSubagentLabel = undefined;
1294
+ activeSubagentAgent = undefined;
1295
+ activeSubagentTask = undefined;
1296
+ activeSubagentStatus = undefined;
1202
1297
  subagentController.reset(ctx);
1203
- contextPanel.resetSession();
1204
1298
  const entries = ctx.sessionManager.getEntries();
1205
1299
  for (let i = entries.length - 1; i >= 0; i--) {
1206
1300
  const entry = entries[i] as { type: string; customType?: string; data?: TakomiState };
@@ -1220,6 +1314,7 @@ ${stateJson}`
1220
1314
  }
1221
1315
 
1222
1316
  syncContextPanelState();
1317
+ contextPanel.rebuildFromSession(ctx);
1223
1318
  await refreshUi(ctx, state);
1224
1319
  contextPanel.show(ctx);
1225
1320
  flushPendingSubagentEvents();
@@ -0,0 +1,296 @@
1
+ import * as fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import {
5
+ BUNDLED_TAKOMI_ROUTING_POLICY_PATH,
6
+ GLOBAL_PI_SETTINGS_PATH,
7
+ GLOBAL_TAKOMI_ROUTING_POLICY_PATH,
8
+ PROJECT_PI_SETTINGS_RELATIVE,
9
+ TAKOMI_ROUTING_POLICY_RELATIVE,
10
+ resolveTakomiRoutingPolicy,
11
+ } from "./routing-policy";
12
+
13
+ export const TAKOMI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
14
+
15
+ type Settings = {
16
+ takomi?: { modelRoutingPolicyFile?: string };
17
+ subagents?: { agentOverrides?: Record<string, unknown> };
18
+ };
19
+
20
+ export type TakomiAgentModelDefault = {
21
+ agent: string;
22
+ model?: string;
23
+ thinking?: string;
24
+ fallbackModels?: string[];
25
+ };
26
+
27
+ export type TakomiModelRoutingSnapshot = {
28
+ approvedModels: string[];
29
+ preferredModels: string[];
30
+ sourceFiles: string[];
31
+ agentDefaults: TakomiAgentModelDefault[];
32
+ };
33
+
34
+ function asRecord(value: unknown): Record<string, unknown> {
35
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
36
+ }
37
+
38
+ function unique(values: string[]): string[] {
39
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
40
+ }
41
+
42
+ async function readSettingsFile(filePath: string): Promise<Settings> {
43
+ try {
44
+ return JSON.parse(await fs.promises.readFile(filePath, "utf8")) as Settings;
45
+ } catch {
46
+ return {};
47
+ }
48
+ }
49
+
50
+ function readSettingsFileSync(filePath: string): Settings {
51
+ try {
52
+ return JSON.parse(fs.readFileSync(filePath, "utf8")) as Settings;
53
+ } catch {
54
+ return {};
55
+ }
56
+ }
57
+
58
+ function mergeSettings(globalSettings: Settings, projectSettings: Settings): Settings {
59
+ const globalOverrides = asRecord(globalSettings.subagents?.agentOverrides);
60
+ const projectOverrides = asRecord(projectSettings.subagents?.agentOverrides);
61
+ return {
62
+ ...globalSettings,
63
+ ...projectSettings,
64
+ takomi: { ...(globalSettings.takomi ?? {}), ...(projectSettings.takomi ?? {}) },
65
+ subagents: {
66
+ ...(globalSettings.subagents ?? {}),
67
+ ...(projectSettings.subagents ?? {}),
68
+ agentOverrides: { ...globalOverrides, ...projectOverrides },
69
+ },
70
+ };
71
+ }
72
+
73
+ async function readSettings(cwd: string): Promise<Settings> {
74
+ const globalSettings = await readSettingsFile(path.join(os.homedir(), ".pi", "agent", "settings.json"));
75
+ const projectSettings = await readSettingsFile(path.resolve(cwd, ".pi/settings.json"));
76
+ return mergeSettings(globalSettings, projectSettings);
77
+ }
78
+
79
+ function readSettingsSync(cwd: string): Settings {
80
+ const globalSettings = readSettingsFileSync(path.join(os.homedir(), ".pi", "agent", "settings.json"));
81
+ const projectSettings = readSettingsFileSync(path.resolve(cwd, ".pi/settings.json"));
82
+ return mergeSettings(globalSettings, projectSettings);
83
+ }
84
+
85
+ function collectAgentDefaultsFromSettings(settings: Settings): TakomiAgentModelDefault[] {
86
+ const overrides = asRecord(settings.subagents?.agentOverrides);
87
+ const defaults: TakomiAgentModelDefault[] = [];
88
+ for (const [agent, value] of Object.entries(overrides)) {
89
+ const record = asRecord(value);
90
+ const fallbackModels = Array.isArray(record.fallbackModels)
91
+ ? record.fallbackModels.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
92
+ : undefined;
93
+ defaults.push({
94
+ agent,
95
+ model: typeof record.model === "string" ? record.model : undefined,
96
+ thinking: typeof record.thinking === "string" ? record.thinking : undefined,
97
+ fallbackModels,
98
+ });
99
+ }
100
+ return defaults;
101
+ }
102
+
103
+ function collectModelsFromDefaults(defaults: TakomiAgentModelDefault[]): string[] {
104
+ const models: string[] = [];
105
+ for (const record of defaults) {
106
+ if (record.model) models.push(stripThinkingSuffix(record.model).baseModel);
107
+ if (Array.isArray(record.fallbackModels)) {
108
+ for (const fallback of record.fallbackModels) models.push(stripThinkingSuffix(fallback).baseModel);
109
+ }
110
+ }
111
+ return models;
112
+ }
113
+
114
+ function isModelLike(value: string): boolean {
115
+ const lower = value.toLowerCase();
116
+ return /(^|\/)(gpt|claude|gemini|o[0-9]|qwen|deepseek|llama|mistral|kimi|grok|sonnet|haiku|opus|codex|mini|max)/i.test(lower)
117
+ || lower.includes("oauth-router/")
118
+ || lower.includes("openai-codex/")
119
+ || lower.includes("lmstudio/");
120
+ }
121
+
122
+ function extractPreferredProvider(text: string): string | undefined {
123
+ const match = text.match(/(?:preferred|default)\s+(?:provider|router)(?:\s*\/\s*(?:provider|router))?\s*:\s*([a-z0-9-]+)/i)
124
+ ?? text.match(/use\s+([a-z0-9-]+)\s+as\s+(?:the\s+)?(?:provider|router)/i);
125
+ return match?.[1];
126
+ }
127
+
128
+ function collectModelsFromPolicy(text: string): string[] {
129
+ // Provider-agnostic policy text like "GPT-5.5" expresses routing intent, not
130
+ // a concrete provider. A preferred provider/router header intentionally binds
131
+ // those intent labels to executable provider-qualified IDs.
132
+ const explicit = (text.match(/[a-z0-9-]+\/[a-z0-9._-]+/gi) ?? []).filter(isModelLike);
133
+ const preferredProvider = extractPreferredProvider(text);
134
+ const inferred: string[] = [];
135
+ if (preferredProvider && /gpt[- ]?5\.5/i.test(text)) inferred.push(`${preferredProvider}/gpt-5.5`);
136
+ if (preferredProvider && /gpt[- ]?5\.4(?!\s*mini)/i.test(text)) inferred.push(`${preferredProvider}/gpt-5.4`);
137
+ if (preferredProvider && /gpt[- ]?5\.4\s*mini/i.test(text)) inferred.push(`${preferredProvider}/gpt-5.4-mini`);
138
+ return unique([...explicit, ...inferred].map((model) => stripThinkingSuffix(model).baseModel));
139
+ }
140
+
141
+ function readPolicyTextSync(filePath: string): string | undefined {
142
+ try {
143
+ const text = fs.readFileSync(filePath, "utf8").trim();
144
+ return text || undefined;
145
+ } catch {
146
+ return undefined;
147
+ }
148
+ }
149
+
150
+ function resolvePolicySync(cwd: string): { policyPath?: string; text?: string } {
151
+ const projectSettings = readSettingsFileSync(path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE));
152
+ const projectTakomi = asRecord(projectSettings.takomi);
153
+ const configuredProject = typeof projectTakomi.modelRoutingPolicyFile === "string"
154
+ ? projectTakomi.modelRoutingPolicyFile
155
+ : TAKOMI_ROUTING_POLICY_RELATIVE;
156
+ const configuredProjectPath = path.isAbsolute(configuredProject) ? configuredProject : path.join(cwd, configuredProject);
157
+ const configuredProjectText = readPolicyTextSync(configuredProjectPath);
158
+ if (configuredProjectText) return { policyPath: configuredProjectPath, text: configuredProjectText };
159
+
160
+ const defaultProjectPath = path.join(cwd, TAKOMI_ROUTING_POLICY_RELATIVE);
161
+ if (path.resolve(defaultProjectPath) !== path.resolve(configuredProjectPath)) {
162
+ const defaultProjectText = readPolicyTextSync(defaultProjectPath);
163
+ if (defaultProjectText) return { policyPath: defaultProjectPath, text: defaultProjectText };
164
+ }
165
+
166
+ const globalSettings = readSettingsFileSync(GLOBAL_PI_SETTINGS_PATH);
167
+ const globalTakomi = asRecord(globalSettings.takomi);
168
+ const configuredGlobal = typeof globalTakomi.modelRoutingPolicyFile === "string"
169
+ ? globalTakomi.modelRoutingPolicyFile
170
+ : GLOBAL_TAKOMI_ROUTING_POLICY_PATH;
171
+ const configuredGlobalPath = path.isAbsolute(configuredGlobal) ? configuredGlobal : path.join(os.homedir(), configuredGlobal);
172
+ const configuredGlobalText = readPolicyTextSync(configuredGlobalPath);
173
+ if (configuredGlobalText) return { policyPath: configuredGlobalPath, text: configuredGlobalText };
174
+
175
+ if (path.resolve(configuredGlobalPath) !== path.resolve(GLOBAL_TAKOMI_ROUTING_POLICY_PATH)) {
176
+ const globalText = readPolicyTextSync(GLOBAL_TAKOMI_ROUTING_POLICY_PATH);
177
+ if (globalText) return { policyPath: GLOBAL_TAKOMI_ROUTING_POLICY_PATH, text: globalText };
178
+ }
179
+
180
+ const bundledText = readPolicyTextSync(BUNDLED_TAKOMI_ROUTING_POLICY_PATH);
181
+ return bundledText ? { policyPath: BUNDLED_TAKOMI_ROUTING_POLICY_PATH, text: bundledText } : {};
182
+ }
183
+
184
+ export function stripThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
185
+ const colonIdx = model.lastIndexOf(":");
186
+ if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
187
+ const suffix = model.slice(colonIdx + 1).toLowerCase();
188
+ if (!(TAKOMI_THINKING_LEVELS as readonly string[]).includes(suffix)) return { baseModel: model, thinkingSuffix: "" };
189
+ return { baseModel: model.slice(0, colonIdx), thinkingSuffix: `:${suffix}` };
190
+ }
191
+
192
+ export function modelFamily(model: string): string {
193
+ const baseModel = stripThinkingSuffix(model).baseModel;
194
+ return baseModel.split("/").at(-1)?.toLowerCase() ?? baseModel.toLowerCase();
195
+ }
196
+
197
+ export function isTakomiModelApproved(requested: string, approved: string[]): boolean {
198
+ const requestedBase = stripThinkingSuffix(requested).baseModel;
199
+ return approved.some((candidate) => stripThinkingSuffix(candidate).baseModel === requestedBase);
200
+ }
201
+
202
+ export function approvedModelEquivalent(requested: string, approved: string[]): string | undefined {
203
+ const { thinkingSuffix } = stripThinkingSuffix(requested);
204
+ const requestedFamily = modelFamily(requested);
205
+ const equivalent = approved.find((candidate) => modelFamily(candidate) === requestedFamily);
206
+ return equivalent ? `${stripThinkingSuffix(equivalent).baseModel}${thinkingSuffix}` : undefined;
207
+ }
208
+
209
+ export function normalizeModelToApproved(requested: string | undefined, approved: string[], fallback?: string): string | undefined {
210
+ if (!requested) return fallback;
211
+ if (isTakomiModelApproved(requested, approved)) return requested;
212
+ return approvedModelEquivalent(requested, approved) ?? fallback ?? requested;
213
+ }
214
+
215
+ function normalizedAgentKey(agent: string): string {
216
+ return agent.toLowerCase().replace(/[^a-z0-9]+/g, "");
217
+ }
218
+
219
+ const DEFAULT_ALIAS_KEYS: Record<string, string[]> = {
220
+ architect: ["architect", "planner", "oracle", "worker"],
221
+ orchestrator: ["orchestrator", "planner", "oracle", "worker"],
222
+ planner: ["planner", "oracle", "worker"],
223
+ coder: ["coder", "code", "worker", "delegate"],
224
+ code: ["code", "coder", "worker", "delegate"],
225
+ designer: ["designer", "design", "worker", "delegate"],
226
+ design: ["design", "designer", "worker", "delegate"],
227
+ reviewer: ["reviewer", "review", "oracle"],
228
+ review: ["review", "reviewer", "oracle"],
229
+ general: ["general", "worker"],
230
+ };
231
+
232
+ export function resolveAgentRoutingDefault(snapshot: TakomiModelRoutingSnapshot, agent: string): TakomiAgentModelDefault | undefined {
233
+ const normalized = normalizedAgentKey(agent);
234
+ const candidates = [normalized, ...(DEFAULT_ALIAS_KEYS[normalized] ?? []), "worker"].map(normalizedAgentKey);
235
+ return snapshot.agentDefaults.find((entry) => candidates.includes(normalizedAgentKey(entry.agent)));
236
+ }
237
+
238
+ export function applyTakomiRoutingDefaults<T extends { agent: string; model?: string; fallbackModels?: string[]; thinking?: string }>(
239
+ task: T,
240
+ snapshot: TakomiModelRoutingSnapshot,
241
+ ): T {
242
+ const defaults = resolveAgentRoutingDefault(snapshot, task.agent);
243
+ if (!snapshot.approvedModels.length && !defaults) return task;
244
+ const approved = snapshot.approvedModels;
245
+ const model = normalizeModelToApproved(task.model, approved, defaults?.model);
246
+ const thinking = task.thinking ?? defaults?.thinking;
247
+ const mergedFallbacks = unique([...(task.fallbackModels ?? []), ...(defaults?.fallbackModels ?? [])]);
248
+ const fallbackModels = unique(mergedFallbacks
249
+ .map((fallback) => normalizeModelToApproved(fallback, approved))
250
+ .filter((fallback): fallback is string => Boolean(fallback))
251
+ .filter((fallback) => !approved.length || isTakomiModelApproved(fallback, approved)));
252
+
253
+ return {
254
+ ...task,
255
+ ...(model ? { model } : {}),
256
+ ...(thinking ? { thinking } : {}),
257
+ ...(fallbackModels.length ? { fallbackModels } : {}),
258
+ };
259
+ }
260
+
261
+ export async function loadTakomiModelRoutingSnapshot(cwd: string): Promise<TakomiModelRoutingSnapshot> {
262
+ const settings = await readSettings(cwd);
263
+ const agentDefaults = collectAgentDefaultsFromSettings(settings);
264
+ const settingsModels = collectModelsFromDefaults(agentDefaults);
265
+ const resolvedPolicy = await resolveTakomiRoutingPolicy(cwd);
266
+ const sourceFiles = resolvedPolicy.policyPath ? [resolvedPolicy.policyPath] : [];
267
+ const policyModels = resolvedPolicy.text ? collectModelsFromPolicy(resolvedPolicy.text) : [];
268
+ const approvedModels = unique([...settingsModels, ...policyModels]);
269
+ return { approvedModels, preferredModels: settingsModels.length ? unique(settingsModels) : approvedModels, sourceFiles, agentDefaults };
270
+ }
271
+
272
+ export function loadTakomiModelRoutingSnapshotSync(cwd: string): TakomiModelRoutingSnapshot {
273
+ const settings = readSettingsSync(cwd);
274
+ const agentDefaults = collectAgentDefaultsFromSettings(settings);
275
+ const settingsModels = collectModelsFromDefaults(agentDefaults);
276
+ const resolvedPolicy = resolvePolicySync(cwd);
277
+ const sourceFiles = resolvedPolicy.policyPath ? [resolvedPolicy.policyPath] : [];
278
+ const policyModels = resolvedPolicy.text ? collectModelsFromPolicy(resolvedPolicy.text) : [];
279
+ const approvedModels = unique([...settingsModels, ...policyModels]);
280
+ return { approvedModels, preferredModels: settingsModels.length ? unique(settingsModels) : approvedModels, sourceFiles, agentDefaults };
281
+ }
282
+
283
+ export function renderCompactTakomiModelRoutingSummary(snapshot: TakomiModelRoutingSnapshot): string {
284
+ if (!snapshot.approvedModels.length && !snapshot.agentDefaults.length) return "";
285
+ const defaultLines = snapshot.agentDefaults
286
+ .filter((entry) => entry.model)
287
+ .map((entry) => `- ${entry.agent}: ${entry.model}${entry.thinking ? ` (${entry.thinking})` : ""}${entry.fallbackModels?.length ? `; fallbacks ${entry.fallbackModels.join(", ")}` : ""}`);
288
+ return [
289
+ "Active Takomi subagent routing summary:",
290
+ snapshot.sourceFiles.length ? `Policy source: ${snapshot.sourceFiles.join(", ")}` : "Policy source: settings/defaults",
291
+ snapshot.approvedModels.length ? `Approved model IDs: ${snapshot.approvedModels.join(", ")}` : "Approved model IDs: none discovered",
292
+ "When calling takomi_subagent, omit model to use these defaults or use only the approved provider-qualified IDs above. Do not use openai-codex/* when an oauth-router/* equivalent is approved.",
293
+ defaultLines.length ? "Role defaults:" : "",
294
+ ...defaultLines,
295
+ ].filter(Boolean).join("\n");
296
+ }
@@ -22,6 +22,15 @@ export type RoutingPolicyInstallResult = {
22
22
  detectedDefaults: string[];
23
23
  };
24
24
 
25
+ export type RoutingPolicyPreviewResult = {
26
+ scope: RoutingPolicyInstallScope;
27
+ policy: string;
28
+ policyPath: string;
29
+ settingsPath: string;
30
+ detectedDefaults: string[];
31
+ overrides: JsonObject;
32
+ };
33
+
25
34
  export type RoutingPolicyInstallScope = "global" | "project";
26
35
  export type RoutingPolicySource = "project" | "global" | "bundled" | "missing";
27
36
 
@@ -64,6 +73,25 @@ function normalizeForSettings(filePath: string): string {
64
73
  return filePath.replaceAll(path.sep, "/");
65
74
  }
66
75
 
76
+ function extractPreferredProvider(policy: string): string | undefined {
77
+ const match = policy.match(/(?:preferred|default)\s+(?:provider|router)(?:\s*\/\s*(?:provider|router))?\s*:\s*([a-z0-9-]+)/i)
78
+ ?? policy.match(/use\s+([a-z0-9-]+)\s+as\s+(?:the\s+)?(?:provider|router)/i);
79
+ return match?.[1];
80
+ }
81
+
82
+ function findExplicitProviderModel(policy: string, family: RegExp): string | undefined {
83
+ const refs = policy.match(/[a-z0-9-]+\/[a-z0-9._-]+/gi) ?? [];
84
+ return refs.find((ref) => family.test(ref));
85
+ }
86
+
87
+ function providerModel(preferredProvider: string | undefined, model: string): string | undefined {
88
+ return preferredProvider ? `${preferredProvider}/${model}` : undefined;
89
+ }
90
+
91
+ function withOptionalModel(model: string | undefined, thinking: string, extra: JsonObject = {}): JsonObject {
92
+ return model ? { model, thinking, ...extra } : { thinking, ...extra };
93
+ }
94
+
67
95
  function deriveSubagentDefaults(policy: string): { overrides: JsonObject; detected: string[] } {
68
96
  const lower = policy.toLowerCase();
69
97
  const has55 = /gpt[- ]?5\.5/.test(lower);
@@ -71,28 +99,29 @@ function deriveSubagentDefaults(policy: string): { overrides: JsonObject; detect
71
99
  const hasMini = /gpt[- ]?5\.4\s*mini/.test(lower);
72
100
  if (!has55 && !has54 && !hasMini) return { overrides: {}, detected: [] };
73
101
 
74
- const model55 = "oauth-router/gpt-5.5";
75
- const model54 = "oauth-router/gpt-5.4";
76
- const modelMini = "oauth-router/gpt-5.4-mini";
102
+ // Keep generated settings provider-agnostic unless the policy explicitly
103
+ // declares provider-qualified models or a preferred provider/router header.
104
+ const preferredProvider = extractPreferredProvider(policy);
105
+ const model55 = findExplicitProviderModel(policy, /gpt[-_.]?5\.5/i) ?? providerModel(preferredProvider, "gpt-5.5");
106
+ const model54 = findExplicitProviderModel(policy, /gpt[-_.]?5\.4(?![-_.]?mini)/i) ?? providerModel(preferredProvider, "gpt-5.4");
107
+ const modelMini = findExplicitProviderModel(policy, /gpt[-_.]?5\.4[-_.]?mini/i) ?? providerModel(preferredProvider, "gpt-5.4-mini");
77
108
  const overrides: JsonObject = {};
78
109
  const detected: string[] = [];
79
110
 
80
111
  if (has55) {
81
- overrides.oracle = { model: model55, thinking: "high" };
82
- overrides.reviewer = { model: model55, thinking: "high" };
83
- overrides.planner = { model: model55, thinking: "medium" };
84
- detected.push("oracle/reviewer → GPT-5.5 high", "planner → GPT-5.5 medium");
112
+ overrides.orchestrator = withOptionalModel(model55, "high");
113
+ overrides.architect = withOptionalModel(model55, "high");
114
+ overrides.reviewer = withOptionalModel(model55, "high");
115
+ detected.push(model55 ? `orchestrator/architect/reviewer → ${model55} high` : "orchestrator/architect/reviewer → GPT-5.5 high intent");
85
116
  }
86
117
  if (has54) {
87
- overrides.worker = { model: model54, thinking: "high", fallbackModels: has55 ? [`${model55}:low`] : undefined };
88
- overrides.contextBuilder = { model: model54, thinking: "high" };
89
- overrides["context-builder"] = { model: model54, thinking: "high" };
90
- detected.push("worker/context-builder → GPT-5.4 high");
118
+ overrides.general = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
119
+ overrides.coder = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
120
+ overrides.designer = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
121
+ detected.push(model54 ? `general/coder/designer → ${model54} high` : "general/coder/designer → GPT-5.4 high intent");
91
122
  }
92
123
  if (hasMini) {
93
- overrides.scout = { model: modelMini, thinking: "high" };
94
- overrides.delegate = { model: modelMini, thinking: "high" };
95
- detected.push("scout/delegate → GPT-5.4 Mini high");
124
+ detected.push(modelMini ? `GPT-5.4 Mini available for explicit small-task overrides: ${modelMini}` : "GPT-5.4 Mini available as small-task intent only");
96
125
  }
97
126
  return { overrides, detected };
98
127
  }
@@ -148,7 +177,7 @@ export async function resolveTakomiRoutingPolicy(cwd: string): Promise<ResolvedR
148
177
  return { source: "missing" };
149
178
  }
150
179
 
151
- export async function installTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): Promise<RoutingPolicyInstallResult> {
180
+ export function previewTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): RoutingPolicyPreviewResult {
152
181
  const policy = extractQuotedPolicy(input);
153
182
  if (!policy) throw new Error("No routing policy text found. Paste the policy after /takomi routing or inside triple quotes.");
154
183
 
@@ -159,6 +188,13 @@ export async function installTakomiRoutingPolicy(cwd: string, input: string, opt
159
188
  const settingsPath = scope === "project"
160
189
  ? path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE)
161
190
  : GLOBAL_PI_SETTINGS_PATH;
191
+ const { overrides, detected } = deriveSubagentDefaults(policy);
192
+ return { scope, policy, policyPath, settingsPath, detectedDefaults: detected, overrides };
193
+ }
194
+
195
+ export async function installTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): Promise<RoutingPolicyInstallResult> {
196
+ const preview = previewTakomiRoutingPolicy(cwd, input, options);
197
+ const { scope, policy, policyPath, settingsPath, overrides, detectedDefaults } = preview;
162
198
  await mkdir(path.dirname(policyPath), { recursive: true });
163
199
  await mkdir(path.dirname(settingsPath), { recursive: true });
164
200
  await writeFile(policyPath, `# Takomi Model Routing Policy\n\n${policy}\n`, "utf8");
@@ -170,7 +206,6 @@ export async function installTakomiRoutingPolicy(cwd: string, input: string, opt
170
206
  : normalizeForSettings(GLOBAL_TAKOMI_ROUTING_POLICY_PATH);
171
207
  settings.takomi = takomi;
172
208
 
173
- const { overrides, detected } = deriveSubagentDefaults(policy);
174
209
  if (Object.keys(overrides).length > 0) {
175
210
  const subagents = asObject(settings.subagents);
176
211
  const existingOverrides = asObject(subagents.agentOverrides);
@@ -179,7 +214,22 @@ export async function installTakomiRoutingPolicy(cwd: string, input: string, opt
179
214
  }
180
215
 
181
216
  await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
182
- return { policyPath, settingsPath, settingsUpdated: true, detectedDefaults: detected };
217
+ return { policyPath, settingsPath, settingsUpdated: true, detectedDefaults };
218
+ }
219
+
220
+ export function renderRoutingPolicyPreview(preview: RoutingPolicyPreviewResult): string {
221
+ const overrideLines = Object.entries(preview.overrides).map(([role, value]) => `- ${role}: ${JSON.stringify(value)}`);
222
+ return [
223
+ `Scope: ${preview.scope}`,
224
+ `Policy path: ${preview.policyPath}`,
225
+ `Settings path: ${preview.settingsPath}`,
226
+ "",
227
+ preview.detectedDefaults.length ? "Detected routing defaults:" : "Detected routing defaults: none",
228
+ ...preview.detectedDefaults.map((item) => `- ${item}`),
229
+ "",
230
+ overrideLines.length ? "Settings overrides to write:" : "Settings overrides to write: none",
231
+ ...overrideLines,
232
+ ].join("\n");
183
233
  }
184
234
 
185
235
  export async function loadTakomiRoutingPolicy(cwd: string): Promise<string | undefined> {