takomi 2.1.45 → 2.5.1

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.
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import type {
5
5
  TakomiDispatchDefaults,
6
6
  TakomiProfile,
7
- TakomiRole,
7
+ TakomiPersona,
8
8
  VibeLifecycleStage,
9
9
  } from "../../../src/pi-takomi-core";
10
10
 
@@ -18,14 +18,15 @@ export const DEFAULT_TAKOMI_PROFILE: TakomiProfile = {
18
18
  roles: {
19
19
  orchestrator: { agent: "orchestrator", dispatchPolicy: "subagent" },
20
20
  architect: { agent: "architect", dispatchPolicy: "subagent" },
21
- design: { agent: "designer", dispatchPolicy: "subagent" },
22
- code: { agent: "coder", dispatchPolicy: "subagent" },
23
- review: { agent: "reviewer", dispatchPolicy: "review-first" },
21
+ designer: { agent: "designer", dispatchPolicy: "subagent" },
22
+ coder: { agent: "coder", dispatchPolicy: "subagent" },
23
+ worker: { agent: "worker", dispatchPolicy: "subagent" },
24
+ reviewer: { agent: "reviewer", dispatchPolicy: "review-first" },
24
25
  },
25
26
  stages: {
26
27
  genesis: { agent: "architect", dispatchPolicy: "subagent" },
27
28
  design: { agent: "designer", dispatchPolicy: "subagent" },
28
- build: { agent: "orchestrator", dispatchPolicy: "subagent" },
29
+ build: { agent: "coder", dispatchPolicy: "subagent" },
29
30
  },
30
31
  review: {
31
32
  enabled: true,
@@ -57,7 +58,7 @@ function mergeDefaults(
57
58
  function mergeProfile(base: TakomiProfile, next?: Partial<TakomiProfile>): TakomiProfile {
58
59
  if (!next) return base;
59
60
  const roles = { ...(base.roles ?? {}) };
60
- for (const [role, defaults] of Object.entries(next.roles ?? {}) as Array<[TakomiRole, TakomiDispatchDefaults]>) {
61
+ for (const [role, defaults] of Object.entries(next.roles ?? {}) as Array<[TakomiPersona, TakomiDispatchDefaults]>) {
61
62
  roles[role] = mergeDefaults(roles[role], defaults);
62
63
  }
63
64
 
@@ -99,12 +100,12 @@ export async function loadTakomiProfile(cwd: string): Promise<TakomiProfile> {
99
100
  const userProfilePath = path.join(os.homedir(), ".pi", "agent", "takomi", "profile.json");
100
101
  const projectProfile = await readProfileFile(projectProfilePath);
101
102
  const userProfile = await readProfileFile(userProfilePath);
102
- return mergeProfile(mergeProfile(DEFAULT_TAKOMI_PROFILE, projectProfile), userProfile);
103
+ return mergeProfile(mergeProfile(DEFAULT_TAKOMI_PROFILE, userProfile), projectProfile);
103
104
  }
104
105
 
105
106
  export function getProfileDefaults(
106
107
  profile: TakomiProfile,
107
- role: TakomiRole,
108
+ role: TakomiPersona,
108
109
  stage?: VibeLifecycleStage,
109
110
  ): TakomiDispatchDefaults {
110
111
  return {
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  export const TAKOMI_ROUTING_POLICY_RELATIVE = path.join(".pi", "takomi", "model-routing.md");
7
- export const GLOBAL_TAKOMI_ROUTING_POLICY_PATH = path.join(os.homedir(), ".pi", "takomi", "model-routing.md");
7
+ export const GLOBAL_TAKOMI_ROUTING_POLICY_PATH = path.join(os.homedir(), ".pi", "agent", "takomi", "model-routing.md");
8
8
  export const GLOBAL_PI_SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
9
9
  export const PROJECT_PI_SETTINGS_RELATIVE = path.join(".pi", "settings.json");
10
10
  export const BUNDLED_TAKOMI_ROUTING_POLICY_PATH = path.resolve(
@@ -99,59 +99,6 @@ function normalizeForSettings(filePath: string): string {
99
99
  return filePath.replaceAll(path.sep, "/");
100
100
  }
101
101
 
102
- function extractPreferredProvider(policy: string): string | undefined {
103
- const match = policy.match(/(?:preferred|default)\s+(?:provider|router)(?:\s*\/\s*(?:provider|router))?\s*:\s*([a-z0-9-]+)/i)
104
- ?? policy.match(/use\s+([a-z0-9-]+)\s+as\s+(?:the\s+)?(?:provider|router)/i);
105
- return match?.[1];
106
- }
107
-
108
- function findExplicitProviderModel(policy: string, family: RegExp): string | undefined {
109
- const refs = policy.match(/[a-z0-9-]+\/[a-z0-9._-]+/gi) ?? [];
110
- return refs.find((ref) => family.test(ref));
111
- }
112
-
113
- function providerModel(preferredProvider: string | undefined, model: string): string | undefined {
114
- return preferredProvider ? `${preferredProvider}/${model}` : undefined;
115
- }
116
-
117
- function withOptionalModel(model: string | undefined, thinking: string, extra: JsonObject = {}): JsonObject {
118
- return model ? { model, thinking, ...extra } : { thinking, ...extra };
119
- }
120
-
121
- function deriveSubagentDefaults(policy: string): { overrides: JsonObject; detected: string[] } {
122
- const lower = policy.toLowerCase();
123
- const has55 = /gpt[- ]?5\.5/.test(lower);
124
- const has54 = /gpt[- ]?5\.4(?!\s*mini)/.test(lower);
125
- const hasMini = /gpt[- ]?5\.4\s*mini/.test(lower);
126
- if (!has55 && !has54 && !hasMini) return { overrides: {}, detected: [] };
127
-
128
- // Keep generated settings provider-agnostic unless the policy explicitly
129
- // declares provider-qualified models or a preferred provider/router header.
130
- const preferredProvider = extractPreferredProvider(policy);
131
- const model55 = findExplicitProviderModel(policy, /gpt[-_.]?5\.5/i) ?? providerModel(preferredProvider, "gpt-5.5");
132
- const model54 = findExplicitProviderModel(policy, /gpt[-_.]?5\.4(?![-_.]?mini)/i) ?? providerModel(preferredProvider, "gpt-5.4");
133
- const modelMini = findExplicitProviderModel(policy, /gpt[-_.]?5\.4[-_.]?mini/i) ?? providerModel(preferredProvider, "gpt-5.4-mini");
134
- const overrides: JsonObject = {};
135
- const detected: string[] = [];
136
-
137
- if (has55) {
138
- overrides.orchestrator = withOptionalModel(model55, "high");
139
- overrides.architect = withOptionalModel(model55, "high");
140
- overrides.reviewer = withOptionalModel(model55, "high");
141
- detected.push(model55 ? `orchestrator/architect/reviewer → ${model55} high` : "orchestrator/architect/reviewer → GPT-5.5 high intent");
142
- }
143
- if (has54) {
144
- overrides.general = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
145
- overrides.coder = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
146
- overrides.designer = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
147
- detected.push(model54 ? `general/coder/designer → ${model54} high` : "general/coder/designer → GPT-5.4 high intent");
148
- }
149
- if (hasMini) {
150
- detected.push(modelMini ? `GPT-5.4 Mini available for explicit small-task overrides: ${modelMini}` : "GPT-5.4 Mini available as small-task intent only");
151
- }
152
- return { overrides, detected };
153
- }
154
-
155
102
  export async function resolveTakomiRoutingPolicy(cwd: string): Promise<ResolvedRoutingPolicy> {
156
103
  const projectSettingsPath = path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE);
157
104
  const projectSettings = await readJsonObject(projectSettingsPath);
@@ -216,9 +163,11 @@ export function previewTakomiRoutingPolicy(cwd: string, input: string, options:
216
163
  const settingsPath = scope === "project"
217
164
  ? path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE)
218
165
  : GLOBAL_PI_SETTINGS_PATH;
219
- const { overrides, detected } = deriveSubagentDefaults(policy);
166
+ const overrides: JsonObject = {};
167
+ const detected: string[] = [];
220
168
 
221
- // Resolve named model families from Pi's registry for review visibility. These
169
+ // Markdown is advisory model-facing guidance. It is never parsed into executable defaults.
170
+ // Resolve named model families from Pi's registry only for preview visibility.
222
171
  // are conditional routing intents, not role-wide defaults, so do not invent
223
172
  // agentOverrides merely to make the extraction non-empty.
224
173
  const availableModels = [...new Set(options.availableModels ?? [])];
@@ -246,13 +195,6 @@ export async function installTakomiRoutingPolicy(cwd: string, input: string, opt
246
195
  : normalizeForSettings(GLOBAL_TAKOMI_ROUTING_POLICY_PATH);
247
196
  settings.takomi = takomi;
248
197
 
249
- if (Object.keys(overrides).length > 0) {
250
- const subagents = asObject(settings.subagents);
251
- const existingOverrides = asObject(subagents.agentOverrides);
252
- subagents.agentOverrides = { ...existingOverrides, ...overrides };
253
- settings.subagents = subagents;
254
- }
255
-
256
198
  await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
257
199
  return { policyPath, settingsPath, settingsUpdated: true, detectedDefaults };
258
200
  }
@@ -264,14 +206,91 @@ export function renderRoutingPolicyPreview(preview: RoutingPolicyPreviewResult):
264
206
  `Policy path: ${preview.policyPath}`,
265
207
  `Settings path: ${preview.settingsPath}`,
266
208
  "",
267
- preview.detectedDefaults.length ? "Detected routing defaults:" : "Detected routing defaults: none",
209
+ preview.detectedDefaults.length ? "Advisory model concepts recognized:" : "Advisory model concepts recognized: none",
268
210
  ...preview.detectedDefaults.map((item) => `- ${item}`),
269
211
  "",
270
- overrideLines.length ? "Settings overrides to write:" : "Settings overrides to write: none",
212
+ overrideLines.length ? "Executable settings changes:" : "Executable settings changes: none (Markdown is advisory only)",
271
213
  ...overrideLines,
272
214
  ].join("\n");
273
215
  }
274
216
 
217
+ export type TakomiRoutingConfigUpdate = {
218
+ defaultProvider?: string;
219
+ approvedModels?: string[];
220
+ roleDefaults?: Record<string, { model?: string; thinking?: string; fallbackModels?: string[] }>;
221
+ };
222
+
223
+ export type TakomiRoutingConfigPreview = {
224
+ scope: RoutingPolicyInstallScope;
225
+ settingsPath: string;
226
+ before: JsonObject;
227
+ after: JsonObject;
228
+ };
229
+
230
+ const CANONICAL_ROUTING_ROLES = new Set(["architect", "designer", "coder", "worker", "reviewer", "orchestrator"]);
231
+
232
+ function validateRoutingConfig(update: TakomiRoutingConfigUpdate, availableModels: string[] = []): void {
233
+ const allConfigured = [
234
+ ...(update.approvedModels ?? []),
235
+ ...Object.values(update.roleDefaults ?? {}).flatMap((entry) => [entry.model, ...(entry.fallbackModels ?? [])]).filter((item): item is string => Boolean(item)),
236
+ ];
237
+ for (const role of Object.keys(update.roleDefaults ?? {})) {
238
+ if (!CANONICAL_ROUTING_ROLES.has(role)) throw new Error(`Unknown Takomi persona '${role}'. Use architect, designer, coder, worker, reviewer, or orchestrator.`);
239
+ }
240
+ for (const model of allConfigured) {
241
+ const base = model.replace(/:(?:off|minimal|low|medium|high|xhigh)$/i, "");
242
+ if (!base.includes("/")) throw new Error(`Model '${model}' must be provider-qualified.`);
243
+ if (availableModels.length && !availableModels.includes(base)) throw new Error(`Model '${base}' is not enabled in Pi's available model registry.`);
244
+ }
245
+ }
246
+
247
+ export async function previewTakomiRoutingConfig(
248
+ cwd: string,
249
+ scope: RoutingPolicyInstallScope,
250
+ update: TakomiRoutingConfigUpdate,
251
+ availableModels: string[] = [],
252
+ ): Promise<TakomiRoutingConfigPreview> {
253
+ validateRoutingConfig(update, availableModels);
254
+ const settingsPath = scope === "project" ? path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE) : GLOBAL_PI_SETTINGS_PATH;
255
+ const settings = await readJsonObject(settingsPath);
256
+ const takomi = asObject(settings.takomi);
257
+ const before = asObject(takomi.routing);
258
+ const existingRoles = asObject(before.roleDefaults);
259
+ const nextRoles = { ...existingRoles };
260
+ for (const [role, value] of Object.entries(update.roleDefaults ?? {})) {
261
+ nextRoles[role] = { ...asObject(existingRoles[role]), ...value };
262
+ }
263
+ const after: JsonObject = {
264
+ ...before,
265
+ ...(update.defaultProvider !== undefined ? { defaultProvider: update.defaultProvider } : {}),
266
+ ...(update.approvedModels !== undefined ? { approvedModels: [...new Set(update.approvedModels)] } : {}),
267
+ ...(update.roleDefaults !== undefined ? { roleDefaults: nextRoles } : {}),
268
+ };
269
+ return { scope, settingsPath, before, after };
270
+ }
271
+
272
+ export async function installTakomiRoutingConfig(preview: TakomiRoutingConfigPreview): Promise<void> {
273
+ const settings = await readJsonObject(preview.settingsPath);
274
+ const takomi = asObject(settings.takomi);
275
+ takomi.routing = preview.after;
276
+ settings.takomi = takomi;
277
+ await mkdir(path.dirname(preview.settingsPath), { recursive: true });
278
+ await writeFile(preview.settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
279
+ }
280
+
281
+ export function renderTakomiRoutingConfigPreview(preview: TakomiRoutingConfigPreview): string {
282
+ return [
283
+ `Scope: ${preview.scope}`,
284
+ `Settings: ${preview.settingsPath}`,
285
+ "",
286
+ "Before:",
287
+ JSON.stringify(preview.before, null, 2),
288
+ "",
289
+ "After:",
290
+ JSON.stringify(preview.after, null, 2),
291
+ ].join("\n");
292
+ }
293
+
275
294
  export async function loadTakomiRoutingPolicy(cwd: string): Promise<string | undefined> {
276
295
  return (await resolveTakomiRoutingPolicy(cwd)).text;
277
296
  }
@@ -443,38 +443,15 @@ function modelCandidates(requested?: string, fallback?: string | string[]): stri
443
443
  return [requested, ...fallbackList].filter(Boolean) as string[];
444
444
  }
445
445
 
446
- export async function resolvePreferredModel(ctx: ExtensionContext, requested?: string, fallback?: string | string[]): Promise<{ model?: string; warning?: string }> {
447
- const candidates = modelCandidates(requested, fallback);
448
- if (candidates.length === 0) return {};
446
+ export async function resolvePreferredModel(ctx: ExtensionContext, requested?: string, _fallback?: string | string[]): Promise<{ model?: string; warning?: string }> {
447
+ if (!requested) return {};
449
448
  const keys = await getAvailableModelKeys(ctx);
450
- const exact = candidates.find((candidate) => keys.includes(candidate));
449
+ const exact = keys.find((key) => key === requested) ?? keys.find((key) => key.toLowerCase() === requested.toLowerCase());
451
450
  if (exact) return { model: exact };
452
-
453
- const loweredKeys = keys.map((key) => key.toLowerCase());
454
- for (const candidate of candidates) {
455
- const idx = loweredKeys.findIndex((key) => key === candidate.toLowerCase());
456
- if (idx >= 0) return { model: keys[idx] };
457
- }
458
-
459
- for (const candidate of candidates) {
460
- const idx = loweredKeys.findIndex((key) => key.includes(candidate.toLowerCase()) || candidate.toLowerCase().includes(key));
461
- if (idx >= 0) {
462
- return {
463
- model: keys[idx],
464
- warning: `Requested model '${candidate}' was unavailable; using '${keys[idx]}' instead.`,
465
- };
466
- }
467
- }
468
-
469
- const firstAvailable = keys.find((key) => key.includes("/"));
470
- if (firstAvailable) {
471
- return {
472
- model: firstAvailable,
473
- warning: `Requested models '${candidates.join("', '")}' were unavailable; using '${firstAvailable}' instead.`,
474
- };
475
- }
476
-
477
- return { warning: `No available signed-in model matched '${candidates.join("', '")}'.` };
451
+ return {
452
+ model: requested,
453
+ warning: `Requested model '${requested}' is not currently enabled in Pi's model registry. The exact provider-qualified ID was preserved; no equivalent provider was substituted.`,
454
+ };
478
455
  }
479
456
 
480
457
  export async function listModelsViaPi(cwd: string, signal?: AbortSignal): Promise<{ ok: boolean; output: string }> {
@@ -47,7 +47,7 @@ type BoardDetails = {
47
47
  };
48
48
 
49
49
  type BoardArgs = {
50
- action?: "init_session" | "expand_stage" | "show_workflows" | "show_session" | "update_task";
50
+ action?: "init_session" | "expand_stage" | "show_workflows" | "show_session" | "update_task" | "replace_master_plan";
51
51
  sessionId?: string;
52
52
  taskId?: string;
53
53
  stage?: string;
@@ -5,6 +5,9 @@ import type { TakomiThinkingLevel } from "../../../src/pi-takomi-core";
5
5
 
6
6
  export type TakomiAgentScope = "user" | "project" | "both";
7
7
 
8
+ export const TAKOMI_PUBLIC_AGENT_NAMES = ["architect", "designer", "coder", "worker", "reviewer", "orchestrator"] as const;
9
+ const TAKOMI_PUBLIC_AGENT_SET = new Set<string>(TAKOMI_PUBLIC_AGENT_NAMES);
10
+
8
11
  export type TakomiAgentConfig = {
9
12
  name: string;
10
13
  description: string;
@@ -53,6 +56,8 @@ function loadAgentsFromDirectory(agentsDir: string, source: "user" | "project"):
53
56
  const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
54
57
  if (!frontmatter.name || !frontmatter.description) continue;
55
58
 
59
+ if (!TAKOMI_PUBLIC_AGENT_SET.has(frontmatter.name)) continue;
60
+
56
61
  agents.push({
57
62
  name: frontmatter.name,
58
63
  description: frontmatter.description,
@@ -45,6 +45,7 @@ const TaskSchema = Type.Object({
45
45
  conversationId: Type.Optional(Type.String()),
46
46
  cwd: Type.Optional(Type.String()),
47
47
  checklist: Type.Optional(Type.Array(Type.Union([Type.String(), ChecklistItemSchema]))),
48
+ requiredCapabilities: Type.Optional(Type.Array(Type.String(), { description: "Capabilities required by the task, such as write-docs or write-code" })),
48
49
  acceptance: Type.Optional(AcceptanceSchema),
49
50
  });
50
51
 
@@ -75,6 +76,7 @@ const SubagentParameters = Type.Object({
75
76
  conversationId: Type.Optional(Type.String({ description: "Persistent conversation id to resume the same subagent session" })),
76
77
  cwd: Type.Optional(Type.String({ description: "Working directory override" })),
77
78
  checklist: Type.Optional(Type.Array(Type.Union([Type.String(), ChecklistItemSchema]), { description: "Optional checklist for the subagent" })),
79
+ requiredCapabilities: Type.Optional(Type.Array(Type.String(), { description: "Capabilities required by the task, such as write-docs or write-code" })),
78
80
  acceptance: Type.Optional(AcceptanceSchema),
79
81
  tasks: Type.Optional(Type.Array(TaskSchema, { description: "Parallel subagent tasks" })),
80
82
  confirmLaunch: Type.Optional(Type.Boolean({ description: "Required to launch immediately in manual Takomi launch mode" })),
@@ -107,6 +109,8 @@ function registerSubagentTool(pi: ExtensionAPI): void {
107
109
  "Set context=fork only when inherited parent-session history is needed; otherwise prefer fresh or the agent default.",
108
110
  "Set async=true for long-running work when the parent can continue safely; keep active worktree writes single-threaded unless worktree isolation is enabled.",
109
111
  "Set clarify=true when the user asks to preview/edit a subagent run in the native Pi TUI before launch.",
112
+ "Use only Takomi's canonical personas: architect, designer, coder, worker, reviewer, and orchestrator.",
113
+ "Set requiredCapabilities for artifact-producing tasks; the runtime blocks inspection-only personas from write-required work.",
110
114
  "Use model, fallbackModels, and thinking only when deliberate; otherwise let the agent/profile defaults apply.",
111
115
  "If review sends work back to the same agent, reuse the same conversationId for continuity.",
112
116
  "If a launch is blocked, cancelled, paused, or review-gated, do not retry automatically; wait for the user's next prompt."
@@ -15,6 +15,7 @@ import { resolveAgentName } from "./agent-aliases";
15
15
  import { applyTakomiRoutingDefaults, loadTakomiModelRoutingSnapshotSync } from "../takomi-runtime/model-routing-defaults";
16
16
  import type { TakomiSubagentToolParams, TakomiSubagentToolTask } from "./tool-runner";
17
17
  import { ensureTakomiAsyncLifecycle, getTakomiAsyncLifecycleSnapshot } from "./async-lifecycle";
18
+ import { TAKOMI_PUBLIC_AGENT_NAMES } from "./agents";
18
19
 
19
20
  type ToolUpdate = (partial: AgentToolResult<Details>) => void;
20
21
 
@@ -159,7 +160,12 @@ function withTakomiAgentDefaults(agent: AgentConfig, cwd: string): AgentConfig {
159
160
  }
160
161
 
161
162
  function discoverUnifiedAgents(discoverPiAgents: any, cwd: string, scope: AgentScope): { agents: AgentConfig[] } {
162
- return { agents: discoverPiAgents(cwd, scope).agents.map((agent: AgentConfig) => withTakomiAgentDefaults(agent, cwd)) };
163
+ const publicNames = new Set<string>(TAKOMI_PUBLIC_AGENT_NAMES);
164
+ return {
165
+ agents: discoverPiAgents(cwd, scope).agents
166
+ .filter((agent: AgentConfig) => publicNames.has(agent.name))
167
+ .map((agent: AgentConfig) => withTakomiAgentDefaults(agent, cwd)),
168
+ };
163
169
  }
164
170
 
165
171
  function agentNameSet(discoverPiAgents: any, cwd: string): Set<string> {
@@ -4,7 +4,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
4
4
  import type { TakomiLaunchMode, TakomiThinkingLevel } from "../../../src/pi-takomi-core";
5
5
  import { loadTakomiProfile } from "../takomi-runtime/profile";
6
6
  import { hasUserGateAutoProvenance } from "../takomi-runtime/gate-provenance";
7
- import { applyTakomiRoutingDefaults, loadTakomiModelRoutingSnapshot } from "../takomi-runtime/model-routing-defaults";
7
+ import { applyTakomiRoutingDefaults, isTakomiModelApproved, loadTakomiModelRoutingSnapshot } from "../takomi-runtime/model-routing-defaults";
8
8
  import { resolveAgentName } from "./agent-aliases";
9
9
  import { discoverTakomiAgents, type TakomiAgentConfig, type TakomiAgentScope } from "./agents";
10
10
  import { createTakomiDelegationPlan, renderTakomiDelegationPlan } from "./delegation-plan";
@@ -26,6 +26,7 @@ export type TakomiSubagentToolTask = {
26
26
  conversationId?: string;
27
27
  cwd?: string;
28
28
  checklist?: ChecklistItem[];
29
+ requiredCapabilities?: string[];
29
30
  acceptance?: TakomiAcceptanceInput;
30
31
  };
31
32
 
@@ -92,6 +93,19 @@ function hasProjectAgents(tasks: Array<{ agent: string }>, agents: Map<string, T
92
93
  return tasks.some((task) => agents.get(task.agent)?.source === "project");
93
94
  }
94
95
 
96
+ function taskRequiresWrite(task: TakomiSubagentToolTask): boolean {
97
+ if (task.requiredCapabilities?.some((capability) => /^(write|edit|write-docs|write-code)$/i.test(capability))) return true;
98
+ return /\b(?:create|write|author|edit|modify|update|implement|fix)\b[\s\S]{0,100}\b(?:file|files|markdown|document|documents|artifact|artifacts|code|configuration)\b/i.test(task.task);
99
+ }
100
+
101
+ function capabilityMismatch(task: TakomiSubagentToolTask, agent: TakomiAgentConfig | undefined): string | undefined {
102
+ if (!agent) return undefined;
103
+ if (taskRequiresWrite(task) && !agent.tools?.some((tool) => tool === "write" || tool === "edit")) {
104
+ return `Task assigned to '${task.agent}' requires file writing, but that persona is inspection-only. Choose architect or designer for their authored Markdown, coder for code, or worker for other writable artifacts.`;
105
+ }
106
+ return undefined;
107
+ }
108
+
95
109
  function hostTrustsProjectAgents(): boolean {
96
110
  return /^(1|true|yes)$/i.test(process.env.TAKOMI_TRUST_PROJECT_AGENTS || "");
97
111
  }
@@ -127,6 +141,7 @@ function compactTaskForFingerprint(task: TakomiSubagentToolTask): Record<string,
127
141
  conversationId: task.conversationId || undefined,
128
142
  cwd: task.cwd,
129
143
  checklist: compactChecklistForFingerprint(task.checklist),
144
+ requiredCapabilities: task.requiredCapabilities?.length ? task.requiredCapabilities : undefined,
130
145
  acceptance: task.acceptance,
131
146
  };
132
147
  }
@@ -332,6 +347,7 @@ function resolveTasks(params: TakomiSubagentToolParams): TakomiSubagentToolTask[
332
347
  conversationId: params.conversationId,
333
348
  cwd: undefined,
334
349
  checklist: params.checklist,
350
+ requiredCapabilities: params.requiredCapabilities,
335
351
  acceptance: params.acceptance,
336
352
  }];
337
353
  }
@@ -405,6 +421,29 @@ export async function executeTakomiSubagentTool(
405
421
  return textResult(message, { results: [], availableAgents: agents.map((agent) => agent.name), agentScope }, true);
406
422
  }
407
423
 
424
+ for (const task of tasks) {
425
+ if (!byName.has(task.agent)) {
426
+ return textResult(
427
+ `Unknown or hidden Takomi persona '${task.agent}'. Available personas: ${agents.map((agent) => agent.name).join(", ") || "none"}.`,
428
+ { results: [], agentScope, task, reason: "unknown-persona" },
429
+ true,
430
+ );
431
+ }
432
+ const mismatch = capabilityMismatch(task, byName.get(task.agent));
433
+ if (mismatch) return textResult(`Blocked by Takomi capability validation.\n\n${mismatch}`, { results: [], agentScope, task, reason: "capability-mismatch" }, true);
434
+ if (task.model && routingSnapshot.approvedModels.length && !isTakomiModelApproved(task.model, routingSnapshot.approvedModels)) {
435
+ return textResult(
436
+ `Blocked by Takomi routing policy. Exact model '${task.model}' is not approved. No provider-equivalent substitution was attempted.`,
437
+ { results: [], agentScope, task, reason: "model-not-approved", approvedModels: routingSnapshot.approvedModels },
438
+ true,
439
+ );
440
+ }
441
+ const invalidFallback = task.fallbackModels?.find((model) => routingSnapshot.approvedModels.length && !isTakomiModelApproved(model, routingSnapshot.approvedModels));
442
+ if (invalidFallback) {
443
+ return textResult(`Blocked by Takomi routing policy. Explicit fallback '${invalidFallback}' is not approved.`, { results: [], agentScope, task, reason: "fallback-not-approved" }, true);
444
+ }
445
+ }
446
+
408
447
  if (!mode) {
409
448
  return textResult(
410
449
  `Provide exactly one mode: agent/task, tasks, or chain.\nAvailable agents: ${agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none"}`,
package/.pi/settings.json CHANGED
@@ -1,42 +1,45 @@
1
1
  {
2
2
  "theme": "takomi-noir",
3
3
  "takomi": {
4
- "modelRoutingPolicyFile": ".pi/takomi/model-routing.md"
5
- },
6
- "subagents": {
7
- "agentOverrides": {
8
- "general": {
9
- "model": "oauth-router/gpt-5.4",
10
- "thinking": "high",
11
- "fallbackModels": [
12
- "oauth-router/gpt-5.5:low"
13
- ]
14
- },
15
- "orchestrator": {
16
- "model": "oauth-router/gpt-5.5",
17
- "thinking": "high"
18
- },
19
- "architect": {
20
- "model": "oauth-router/gpt-5.5",
21
- "thinking": "high"
22
- },
23
- "designer": {
24
- "model": "oauth-router/gpt-5.4",
25
- "thinking": "high",
26
- "fallbackModels": [
27
- "oauth-router/gpt-5.5:low"
28
- ]
29
- },
30
- "coder": {
31
- "model": "oauth-router/gpt-5.4",
32
- "thinking": "high",
33
- "fallbackModels": [
34
- "oauth-router/gpt-5.5:low"
35
- ]
36
- },
37
- "reviewer": {
38
- "model": "oauth-router/gpt-5.5",
39
- "thinking": "high"
4
+ "modelRoutingPolicyFile": ".pi/takomi/model-routing.md",
5
+ "routing": {
6
+ "defaultProvider": "openai-codex",
7
+ "approvedModels": [
8
+ "openai-codex/gpt-5.6-luna",
9
+ "openai-codex/gpt-5.6-sol",
10
+ "openai-codex/gpt-5.6-terra"
11
+ ],
12
+ "roleDefaults": {
13
+ "orchestrator": {
14
+ "model": "openai-codex/gpt-5.6-sol",
15
+ "thinking": "high"
16
+ },
17
+ "architect": {
18
+ "model": "openai-codex/gpt-5.6-sol",
19
+ "thinking": "high"
20
+ },
21
+ "designer": {
22
+ "model": "openai-codex/gpt-5.6-sol",
23
+ "thinking": "medium"
24
+ },
25
+ "coder": {
26
+ "model": "openai-codex/gpt-5.6-terra",
27
+ "thinking": "high",
28
+ "fallbackModels": [
29
+ "openai-codex/gpt-5.6-sol:low"
30
+ ]
31
+ },
32
+ "worker": {
33
+ "model": "openai-codex/gpt-5.6-luna",
34
+ "thinking": "high",
35
+ "fallbackModels": [
36
+ "openai-codex/gpt-5.6-sol:low"
37
+ ]
38
+ },
39
+ "reviewer": {
40
+ "model": "openai-codex/gpt-5.6-sol",
41
+ "thinking": "high"
42
+ }
40
43
  }
41
44
  }
42
45
  }
@@ -1,5 +1,11 @@
1
1
  # Takomi Model Routing Policy
2
2
 
3
+ ## Authority Boundary
4
+
5
+ This file is natural-language guidance for model judgment. It explains when Sol, Terra, Luna, or a specialist route fits a task. It does not define executable allowlists, providers, fallbacks, or persona defaults; those live under `takomi.routing` in global/project `.pi/settings.json`.
6
+
7
+ Takomi's public personas are Architect, Designer, Coder, Worker, Reviewer, and Orchestrator. Genesis, Design, and Build are lifecycle stages. Design always means UI/UX, never application architecture.
8
+
3
9
  ## Goal
4
10
 
5
11
  Choose the cheapest route likely to complete the task correctly.
@@ -1,50 +1,54 @@
1
- {
2
- "version": 1,
3
- "autoOrchestrate": true,
4
- "launchMode": "auto",
5
- "foreground": true,
6
- "background": true,
7
- "reviewAfterImplementation": true,
8
- "roles": {
9
- "orchestrator": {
10
- "agent": "orchestrator",
11
- "dispatchPolicy": "subagent"
12
- },
13
- "architect": {
14
- "agent": "architect",
15
- "dispatchPolicy": "subagent"
16
- },
17
- "design": {
18
- "agent": "designer",
19
- "dispatchPolicy": "subagent"
20
- },
21
- "code": {
22
- "agent": "coder",
23
- "dispatchPolicy": "subagent"
24
- },
25
- "review": {
26
- "agent": "reviewer",
27
- "dispatchPolicy": "review-first"
28
- }
29
- },
30
- "stages": {
31
- "genesis": {
32
- "agent": "architect",
33
- "dispatchPolicy": "subagent"
34
- },
35
- "design": {
36
- "agent": "designer",
37
- "dispatchPolicy": "subagent"
38
- },
39
- "build": {
40
- "agent": "orchestrator",
41
- "dispatchPolicy": "subagent"
42
- }
43
- },
44
- "review": {
45
- "enabled": true,
46
- "agent": "reviewer",
47
- "maxIterations": 2,
48
- "sameConversation": true
49
- }
50
- }
1
+ {
2
+ "version": 1,
3
+ "autoOrchestrate": true,
4
+ "launchMode": "auto",
5
+ "foreground": true,
6
+ "background": true,
7
+ "reviewAfterImplementation": true,
8
+ "roles": {
9
+ "orchestrator": {
10
+ "agent": "orchestrator",
11
+ "dispatchPolicy": "subagent"
12
+ },
13
+ "architect": {
14
+ "agent": "architect",
15
+ "dispatchPolicy": "subagent"
16
+ },
17
+ "designer": {
18
+ "agent": "designer",
19
+ "dispatchPolicy": "subagent"
20
+ },
21
+ "coder": {
22
+ "agent": "coder",
23
+ "dispatchPolicy": "subagent"
24
+ },
25
+ "worker": {
26
+ "agent": "worker",
27
+ "dispatchPolicy": "subagent"
28
+ },
29
+ "reviewer": {
30
+ "agent": "reviewer",
31
+ "dispatchPolicy": "review-first"
32
+ }
33
+ },
34
+ "stages": {
35
+ "genesis": {
36
+ "agent": "architect",
37
+ "dispatchPolicy": "subagent"
38
+ },
39
+ "design": {
40
+ "agent": "designer",
41
+ "dispatchPolicy": "subagent"
42
+ },
43
+ "build": {
44
+ "agent": "coder",
45
+ "dispatchPolicy": "subagent"
46
+ }
47
+ },
48
+ "review": {
49
+ "enabled": true,
50
+ "agent": "reviewer",
51
+ "maxIterations": 2,
52
+ "sameConversation": true
53
+ }
54
+ }