takomi 2.1.32 → 2.1.34

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.
Files changed (54) hide show
  1. package/.pi/extensions/oauth-router/config.ts +14 -5
  2. package/.pi/extensions/oauth-router/index.ts +130 -130
  3. package/.pi/extensions/oauth-router/provider.ts +29 -0
  4. package/.pi/extensions/oauth-router/state.ts +372 -372
  5. package/.pi/extensions/oauth-router/types.ts +1 -1
  6. package/.pi/extensions/takomi-runtime/command-text.ts +2 -4
  7. package/.pi/extensions/takomi-runtime/commands.ts +15 -21
  8. package/.pi/extensions/takomi-runtime/index.ts +127 -53
  9. package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +296 -296
  10. package/.pi/extensions/takomi-runtime/ui.ts +18 -11
  11. package/.pi/extensions/takomi-subagents/index.ts +2 -0
  12. package/.pi/extensions/takomi-subagents/native-render.ts +27 -5
  13. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +1 -1
  14. package/.pi/extensions/takomi-subagents/tool-runner.ts +1 -0
  15. package/assets/.agent/skills/photo-book-builder/SKILL.md +96 -0
  16. package/assets/.agent/skills/photo-book-builder/references/layout_templates.md +72 -0
  17. package/assets/.agent/skills/photo-book-builder/scripts/create_full_bleed_layouts.py +212 -0
  18. package/assets/.agent/skills/photo-book-builder/scripts/organize_photos.py +99 -0
  19. package/assets/.agent/skills/photo-book-builder/scripts/revert_organization.py +61 -0
  20. package/assets/.agent/skills/photo-book-builder/scripts/upscale_covers.py +47 -0
  21. package/assets/.agent/skills/youtube-pipeline/SKILL.md +73 -62
  22. package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Application.md +1 -0
  23. package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phase 1.md +86 -0
  24. package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phase 2.md +106 -0
  25. package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phase 3.md +112 -0
  26. package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phase 4.md +90 -0
  27. package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phased Outline.md +58 -0
  28. package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Repurposing.md +1 -0
  29. package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Research.md +438 -0
  30. package/assets/.agent/skills/youtube-pipeline/resources/prompts/Shorts Bridge Protocol.md +159 -0
  31. package/assets/.agent/skills/youtube-pipeline/resources/prompts/Title Thumbnail Picker Prompt.md +144 -0
  32. package/assets/.agent/skills/youtube-pipeline/resources/prompts/Transcription Extraction Prompt v2.md +190 -0
  33. package/assets/.agent/skills/youtube-pipeline/resources/prompts/Transcription Extraction Prompt.md +156 -0
  34. package/assets/.agent/skills/youtube-pipeline/resources/prompts/Video QA Prompt.md +133 -0
  35. package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase1-strategy.md +28 -18
  36. package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase2-packaging.md +2 -2
  37. package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase3-scripting.md +2 -2
  38. package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase3.5-shorts.md +2 -2
  39. package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase4-production.md +2 -2
  40. package/assets/.agent/skills/youtube-pipeline/resources/youtube-pipeline.md +15 -15
  41. package/assets/.agent/skills/youtube-pipeline/scripts/google-trends/package.json +17 -0
  42. package/assets/.agent/skills/youtube-pipeline/scripts/google-trends/pnpm-lock.yaml +31 -0
  43. package/assets/.agent/skills/youtube-pipeline/scripts/google-trends/search.js +168 -0
  44. package/package.json +1 -1
  45. package/src/cli.js +13 -4
  46. package/src/pi-harness.js +36 -14
  47. package/src/pi-optional-features.js +190 -190
  48. package/src/postinstall.js +27 -27
  49. package/src/skills-catalog.js +245 -245
  50. package/src/skills-installer.js +244 -244
  51. package/src/skills-selection-tui.js +200 -200
  52. package/src/store.js +418 -418
  53. package/src/takomi-stats.d.ts +3 -3
  54. package/src/takomi-stats.js +442 -35
@@ -1,296 +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
- }
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
+ }