takomi 2.1.2 → 2.1.4

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 (52) hide show
  1. package/.pi/README.md +124 -124
  2. package/.pi/agents/architect.md +15 -15
  3. package/.pi/agents/coder.md +14 -14
  4. package/.pi/agents/designer.md +17 -17
  5. package/.pi/agents/orchestrator.md +22 -22
  6. package/.pi/agents/reviewer.md +16 -16
  7. package/.pi/extensions/oauth-router/README.md +125 -125
  8. package/.pi/extensions/oauth-router/commands.ts +380 -380
  9. package/.pi/extensions/oauth-router/config.ts +200 -200
  10. package/.pi/extensions/oauth-router/index.ts +41 -41
  11. package/.pi/extensions/oauth-router/oauth-flow.ts +154 -154
  12. package/.pi/extensions/oauth-router/oauth-store.ts +121 -121
  13. package/.pi/extensions/oauth-router/package.json +14 -14
  14. package/.pi/extensions/oauth-router/policies.ts +27 -27
  15. package/.pi/extensions/oauth-router/provider.ts +492 -492
  16. package/.pi/extensions/oauth-router/scripts/vibe-verify.py +98 -98
  17. package/.pi/extensions/oauth-router/state.ts +174 -174
  18. package/.pi/extensions/oauth-router/types.ts +153 -153
  19. package/.pi/extensions/takomi-runtime/command-text.ts +130 -130
  20. package/.pi/extensions/takomi-runtime/commands.ts +179 -179
  21. package/.pi/extensions/takomi-runtime/context-panel.ts +282 -282
  22. package/.pi/extensions/takomi-runtime/index.ts +1288 -1288
  23. package/.pi/extensions/takomi-runtime/profile.ts +114 -114
  24. package/.pi/extensions/takomi-runtime/routing-policy.ts +105 -105
  25. package/.pi/extensions/takomi-runtime/shared.ts +511 -492
  26. package/.pi/extensions/takomi-runtime/subagent-controller.ts +364 -364
  27. package/.pi/extensions/takomi-runtime/subagent-render.ts +501 -501
  28. package/.pi/extensions/takomi-runtime/subagent-types.ts +90 -83
  29. package/.pi/extensions/takomi-runtime/ui.ts +133 -133
  30. package/.pi/extensions/takomi-subagents/agent-aliases.ts +18 -18
  31. package/.pi/extensions/takomi-subagents/agents.ts +113 -113
  32. package/.pi/extensions/takomi-subagents/delegation-plan.ts +95 -95
  33. package/.pi/extensions/takomi-subagents/dispatch-helpers.ts +26 -26
  34. package/.pi/extensions/takomi-subagents/dispatch.ts +306 -215
  35. package/.pi/extensions/takomi-subagents/index.ts +76 -75
  36. package/.pi/extensions/takomi-subagents/live-updates.ts +136 -83
  37. package/.pi/extensions/takomi-subagents/native-render.ts +5 -142
  38. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +228 -0
  39. package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -209
  40. package/.pi/themes/takomi-noir.json +81 -81
  41. package/package.json +59 -59
  42. package/src/cli.js +14 -0
  43. package/src/doctor.js +87 -84
  44. package/src/pi-harness.js +355 -351
  45. package/src/pi-installer.js +193 -171
  46. package/src/pi-takomi-core/index.ts +4 -4
  47. package/src/pi-takomi-core/orchestration.ts +402 -402
  48. package/src/pi-takomi-core/routing.ts +93 -93
  49. package/src/pi-takomi-core/types.ts +173 -173
  50. package/src/pi-takomi-core/workflows.ts +299 -299
  51. package/src/skills-installer.js +101 -101
  52. package/src/update-check.js +140 -0
@@ -1,1288 +1,1288 @@
1
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import type { AssistantMessage } from "@mariozechner/pi-ai";
4
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
- import { StringEnum } from "@mariozechner/pi-ai";
6
- import { Type } from "typebox";
7
- import {
8
- buildSessionState,
9
- createSessionId,
10
- createLifecycleStarterSession,
11
- createTask,
12
- decideRoute,
13
- getSessionPaths,
14
- getNextTaskId,
15
- getWorkflowDefinition,
16
- listWorkflowDefinitions,
17
- markStageExpanded,
18
- normalizeSessionState,
19
- renderMasterPlan,
20
- renderTaskFile,
21
- serializeSessionState,
22
- slugifyTaskTitle,
23
- type OrchestratorTask,
24
- type OrchestratorSessionState,
25
- type OrchestratorTaskStatus,
26
- type TakomiDispatchPolicy,
27
- type TakomiLaunchMode,
28
- type TakomiProfile,
29
- type TakomiRole,
30
- type TakomiThinkingLevel,
31
- type TakomiWorkflowId,
32
- type VibeLifecycleStage,
33
- } from "../../../src/pi-takomi-core";
34
- import { discoverProjectAgents, type TakomiAgentConfig } from "../takomi-subagents/agents";
35
- import { dispatchTakomiSubagent, type TakomiDispatchResult } from "../takomi-subagents/dispatch";
36
- import { createTakomiDelegationPlan, renderTakomiDelegationPlan } from "../takomi-subagents/delegation-plan";
37
- import { executeTakomiSubagentTool } from "../takomi-subagents/tool-runner";
38
- import {
39
- renderRuntimeStatus,
40
- renderRuntimeWidget,
41
- TakomiFooterComponent,
42
- } from "./ui";
43
- import { getTakomiSubagentController } from "./subagent-controller";
44
- import {
45
- TAKOMI_SUBAGENT_EVENT_CHANNEL,
46
- type TakomiSubagentRunPatch,
47
- type TakomiSubagentRuntimeEvent,
48
- } from "./subagent-types";
49
- import {
50
- visibleWidth,
51
- truncateToWidth,
52
- formatFooterNumber,
53
- buildTaskPrompt,
54
- resolvePreferredModel,
55
- } from "./shared";
56
- import { TakomiContextPanel, wireContextPanel } from "./context-panel";
57
- import { registerTakomiCommands } from "./commands";
58
- import {
59
- DEFAULT_TAKOMI_PROFILE,
60
- getProfileDefaults,
61
- loadTakomiProfile,
62
- } from "./profile";
63
- import { installTakomiRoutingPolicy, loadTakomiRoutingPolicy } from "./routing-policy";
64
-
65
- type TakomiState = {
66
- enabled: boolean;
67
- autoOrch: boolean;
68
- launchMode: TakomiLaunchMode;
69
- planMode: boolean;
70
- role: TakomiRole;
71
- stage?: VibeLifecycleStage;
72
- workflow?: TakomiWorkflowId;
73
- activeSessionId?: string;
74
- subagentsEnabled: boolean;
75
- };
76
-
77
- const DEFAULT_STATE: TakomiState = {
78
- enabled: true,
79
- autoOrch: true,
80
- launchMode: "auto",
81
- planMode: false,
82
- role: "general",
83
- subagentsEnabled: true,
84
- };
85
-
86
- const STATE_ENTRY = "takomi-runtime-state";
87
-
88
- let activeProfile: TakomiProfile = DEFAULT_TAKOMI_PROFILE;
89
- let activeSubagentLabel: string | undefined;
90
-
91
- const ThinkingSchema = Type.Union([
92
- Type.Literal("off"),
93
- Type.Literal("minimal"),
94
- Type.Literal("low"),
95
- Type.Literal("medium"),
96
- Type.Literal("high"),
97
- Type.Literal("xhigh"),
98
- ]);
99
-
100
- function cloneState(state: TakomiState): TakomiState {
101
- return { ...state };
102
- }
103
-
104
- function formatState(state: TakomiState): string {
105
- return [
106
- `Takomi ${state.enabled ? "on" : "off"}`,
107
- `role=${state.role}`,
108
- `stage=${state.stage ?? "-"}`,
109
- `workflow=${state.workflow ?? "-"}`,
110
- `autoOrch=${state.autoOrch ? "on" : "off"}`,
111
- `launch=${state.launchMode}`,
112
- `plan=${state.planMode ? "on" : "off"}`,
113
- `subagents=${state.subagentsEnabled ? "on" : "off"}`,
114
- state.activeSessionId ? `session=${state.activeSessionId}` : "",
115
- ].filter(Boolean).join(" | ");
116
- }
117
-
118
- function setStageAndWorkflow(state: TakomiState, stage: VibeLifecycleStage, options?: { preserveRole?: boolean }) {
119
- state.stage = stage;
120
- state.workflow = stage === "genesis" ? "vibe-genesis" : stage === "design" ? "vibe-design" : "vibe-build";
121
- if (!options?.preserveRole) {
122
- state.role = stage === "design" ? "design" : stage === "build" ? "orchestrator" : "architect";
123
- }
124
- state.enabled = true;
125
- }
126
-
127
- function rolePrompt(role: TakomiRole): string {
128
- switch (role) {
129
- case "orchestrator":
130
- return [
131
- "You are operating in Takomi orchestrator mode.",
132
- "Break work into tasks, delegate with specialist agents, review outputs, and route revisions intelligently.",
133
- "When a task needs more work, you may send it back to the same agent using the same conversation continuity if that is most efficient.",
134
- ].join("\n");
135
- case "architect":
136
- return [
137
- "You are operating in Takomi architect mode.",
138
- "Clarify scope, define acceptance criteria, and build the project foundation before design or implementation.",
139
- ].join("\n");
140
- case "design":
141
- return [
142
- "You are operating in Takomi design mode.",
143
- "Translate genesis context into build-ready UX and visual direction.",
144
- "Prefer Gemini or a similarly strong design-oriented model if available.",
145
- ].join("\n");
146
- case "code":
147
- return [
148
- "You are operating in Takomi code mode.",
149
- "Implement directly, keep scope controlled, and verify after changes.",
150
- ].join("\n");
151
- case "review":
152
- return [
153
- "You are operating in Takomi review mode.",
154
- "Focus on correctness, risk, omissions, and actionable review feedback.",
155
- ].join("\n");
156
- default:
157
- return [
158
- "You are operating in Takomi general mode.",
159
- "Choose the correct lifecycle stage and specialist behavior based on the request.",
160
- ].join("\n");
161
- }
162
- }
163
-
164
- function planPrompt(): string {
165
- return [
166
- "Takomi planning mode is active.",
167
- "Before major implementation, produce a short numbered plan.",
168
- "If the request is broad, explicitly identify whether the user is in genesis, design, or build.",
169
- ].join("\n");
170
- }
171
-
172
- function getInjectedPlaybook(state: TakomiState): string | undefined {
173
- if (!state.workflow) return undefined;
174
- const workflow = getWorkflowDefinition(state.workflow);
175
- return [
176
- `${workflow.title} is the active Takomi workflow.`,
177
- workflow.purpose,
178
- workflow.preferredModelHint ?? "",
179
- workflow.playbook,
180
- workflow.nextStage ? `After this stage, recommend ${workflow.nextStage}.` : "",
181
- ].filter(Boolean).join("\n\n");
182
- }
183
-
184
- function shouldAutoRoute(text: string): boolean {
185
- const lowered = text.toLowerCase();
186
- const broadSignal = ["use takomi", "orchestrate", "plan and build", "full workflow", "break this down", "coordinate"].some((signal) => lowered.includes(signal));
187
- const multiClause = (lowered.match(/\b(and|then|also|after|while)\b/g) ?? []).length >= 2;
188
- return broadSignal || (lowered.length > 220 && multiClause);
189
- }
190
-
191
- function buildTaskRows(tasks: OrchestratorTask[]): string {
192
- return tasks.map((task) => `${task.id}: ${task.stage ?? "-"} | ${task.title} [${task.status}] -> ${task.preferredAgent ?? task.role}${task.conversationId ? ` (${task.conversationId})` : ""}${task.workflow ? ` | workflow=${task.workflow}` : ""}${task.preferredModel ? ` | model=${task.preferredModel}` : ""}${task.preferredThinking ? ` | thinking=${task.preferredThinking}` : ""}${task.dispatchPolicy ? ` | dispatch=${task.dispatchPolicy}` : ""}${task.skills?.length ? ` | skills=${task.skills.join(",")}` : ""}`).join("\n");
193
- }
194
-
195
- function resolveTaskAgent(task: OrchestratorTask): string {
196
- return task.preferredAgent ?? (task.role === "code" ? "coder" : task.role === "design" ? "designer" : task.role === "architect" ? "architect" : task.role === "review" ? "reviewer" : "orchestrator");
197
- }
198
-
199
- function appendTaskNote(existing: string | undefined, heading: string, body?: string): string {
200
- if (!body?.trim()) return existing ?? "";
201
- return [existing, "", `${heading}:`, body.trim()].filter(Boolean).join("\n").trim();
202
- }
203
-
204
- function applyChecklistUpdates(
205
- current: OrchestratorTask["checklist"],
206
- updates?: Array<{ text?: string; index?: number; done?: boolean }>,
207
- ): OrchestratorTask["checklist"] {
208
- if (!current?.length || !updates?.length) return current;
209
- const next = current.map((item: NonNullable<OrchestratorTask["checklist"]>[number]) => ({ ...item }));
210
- for (const update of updates) {
211
- const idx = typeof update.index === "number"
212
- ? update.index
213
- : typeof update.text === "string"
214
- ? next.findIndex((item: NonNullable<OrchestratorTask["checklist"]>[number]) => item.text === update.text)
215
- : -1;
216
- if (idx >= 0 && next[idx]) next[idx] = { ...next[idx], done: update.done ?? next[idx].done };
217
- }
218
- return next;
219
- }
220
-
221
- function normalizeChecklistInput(
222
- checklist?: Array<string | { text: string; done?: boolean }>,
223
- ): OrchestratorTask["checklist"] {
224
- if (!checklist?.length) return undefined;
225
- return checklist.map((item) => typeof item === "string" ? { text: item, done: false } : { text: item.text, done: item.done ?? false });
226
- }
227
-
228
- function resolveChecklistState(
229
- current: OrchestratorTask["checklist"],
230
- nextChecklist?: Array<string | { text: string; done?: boolean }>,
231
- updates?: Array<{ text?: string; index?: number; done?: boolean }>,
232
- ): OrchestratorTask["checklist"] {
233
- const baseChecklist = nextChecklist ? normalizeChecklistInput(nextChecklist) : current;
234
- return applyChecklistUpdates(baseChecklist, updates);
235
- }
236
-
237
- function getIncompleteChecklistItems(checklist?: OrchestratorTask["checklist"]): string[] {
238
- return (checklist ?? [])
239
- .filter((item) => !item.done)
240
- .map((item) => item.text);
241
- }
242
-
243
- function getCompletionGateError(task: Pick<OrchestratorTask, "id" | "title" | "checklist">): string | undefined {
244
- if (!task.checklist?.length) {
245
- return `Task ${task.id} cannot be marked completed until it has a checklist.`;
246
- }
247
- const incompleteItems = getIncompleteChecklistItems(task.checklist);
248
- if (incompleteItems.length === 0) return undefined;
249
- return [
250
- `Task ${task.id} cannot be marked completed until every checklist item is done.`,
251
- "",
252
- "Incomplete checklist items:",
253
- ...incompleteItems.map((item) => `- ${item}`),
254
- ].join("\n");
255
- }
256
-
257
- function getTaskFolder(paths: ReturnType<typeof getSessionPaths>, status: OrchestratorTask["status"]) {
258
- switch (status) {
259
- case "in-progress":
260
- return paths.inProgress;
261
- case "completed":
262
- return paths.completed;
263
- case "blocked":
264
- return paths.blocked;
265
- case "pending":
266
- default:
267
- return paths.pending;
268
- }
269
- }
270
-
271
- function getTaskFileName(task: OrchestratorTask): string {
272
- return `${task.id}_${slugifyTaskTitle(task.title)}.task.md`;
273
- }
274
-
275
- function buildSubagentTaskPrompt(task: OrchestratorTask, extraInstructions?: string): string {
276
- return buildTaskPrompt({
277
- task: extraInstructions?.trim() || task.notes || task.title,
278
- workflow: task.workflow,
279
- skills: task.skills,
280
- checklist: task.checklist,
281
- stage: task.stage,
282
- });
283
- }
284
-
285
- async function hasGenesisArtifacts(cwd: string): Promise<boolean> {
286
- try {
287
- await readFile(path.join(cwd, "docs", "Project_Requirements.md"), "utf8");
288
- await readFile(path.join(cwd, "docs", "Coding_Guidelines.md"), "utf8");
289
- const issues = await readdir(path.join(cwd, "docs", "issues"));
290
- return issues.some((entry) => entry.endsWith(".md"));
291
- } catch {
292
- return false;
293
- }
294
- }
295
-
296
- async function loadSessionState(cwd: string, sessionId: string): Promise<{ state: OrchestratorSessionState; paths: ReturnType<typeof getSessionPaths> }> {
297
- const paths = getSessionPaths(cwd, sessionId);
298
- const raw = await readFile(paths.stateFile, "utf8");
299
- const parsed = JSON.parse(raw) as Partial<OrchestratorSessionState>;
300
- const state = normalizeSessionState({
301
- sessionId,
302
- title: parsed.title ?? "Takomi Session",
303
- ...parsed,
304
- });
305
- return { state, paths };
306
- }
307
-
308
- async function syncTaskArtifacts(cwd: string, session: OrchestratorSessionState) {
309
- const normalizedState = normalizeSessionState(session);
310
- const paths = getSessionPaths(cwd, normalizedState.sessionId);
311
- await mkdir(paths.pending, { recursive: true });
312
- await mkdir(paths.inProgress, { recursive: true });
313
- await mkdir(paths.completed, { recursive: true });
314
- await mkdir(paths.blocked, { recursive: true });
315
- await mkdir(paths.stateDir, { recursive: true });
316
- await writeFile(paths.masterPlan, renderMasterPlan(normalizedState), "utf8");
317
- await writeFile(paths.summary, [
318
- `# Orchestrator Summary: ${normalizedState.title}`,
319
- "",
320
- `- Session ID: ${normalizedState.sessionId}`,
321
- `- Human docs: ${paths.root}`,
322
- `- Machine state: ${paths.stateFile}`,
323
- `- Runtime mode: ${normalizedState.mode}`,
324
- `- Session intent: ${normalizedState.sessionIntent ?? "full-project"}`,
325
- ].join("\n"), "utf8");
326
- await writeFile(paths.stateFile, serializeSessionState(normalizedState), "utf8");
327
-
328
- for (const folder of [paths.pending, paths.inProgress, paths.completed, paths.blocked]) {
329
- const entries = await readdir(folder).catch(() => [] as string[]);
330
- for (const entry of entries) {
331
- if (entry.endsWith(".task.md")) {
332
- await rm(path.join(folder, entry), { force: true });
333
- }
334
- }
335
- }
336
-
337
- for (const task of normalizedState.tasks) {
338
- const filePath = path.join(getTaskFolder(paths, task.status), getTaskFileName(task));
339
- await writeFile(filePath, renderTaskFile(task, `Parent session: ${normalizedState.sessionId}\n\nTask title: ${task.title}`), "utf8");
340
- }
341
-
342
- return paths;
343
- }
344
-
345
- async function writeOrchestratorSession(cwd: string, session: OrchestratorSessionState) {
346
- return syncTaskArtifacts(cwd, session);
347
- }
348
-
349
- type IncomingTask = {
350
- id?: string;
351
- title: string;
352
- role: TakomiRole;
353
- stage?: VibeLifecycleStage;
354
- workflow?: TakomiWorkflowId;
355
- parentTaskId?: string;
356
- preferredAgent?: string;
357
- preferredModel?: string;
358
- preferredModelHint?: string;
359
- preferredThinking?: TakomiThinkingLevel;
360
- fallbackModels?: string[];
361
- dispatchPolicy?: TakomiDispatchPolicy;
362
- skills?: string[];
363
- checklist?: Array<string | { text: string; done?: boolean }>;
364
- objective?: string;
365
- scope?: string[];
366
- definitionOfDone?: string[];
367
- expectedArtifacts?: string[];
368
- dependencies?: string[];
369
- reviewCheckpoint?: string;
370
- instructions?: string[];
371
- conversationId?: string;
372
- };
373
-
374
- async function materializeTasksFromInput(
375
- ctx: ExtensionContext,
376
- currentTasks: OrchestratorTask[],
377
- incoming: IncomingTask[],
378
- stageOverride?: VibeLifecycleStage,
379
- ): Promise<OrchestratorTask[]> {
380
- const nextTasks = [...currentTasks];
381
-
382
- for (const task of incoming) {
383
- const stage = task.stage ?? stageOverride;
384
- const defaults = getProfileDefaults(activeProfile, task.role, stage);
385
- const fallbackModels = [
386
- ...(task.fallbackModels ?? []),
387
- ...(defaults.fallbackModels ?? []),
388
- ];
389
- const requestedModel = task.preferredModel ?? defaults.model;
390
- const resolvedModel = await resolvePreferredModel(ctx, requestedModel, fallbackModels);
391
- const id = task.id ?? getNextTaskId(nextTasks);
392
- nextTasks.push(createTask(id, task.title, task.role, {
393
- stage,
394
- workflow: task.workflow,
395
- parentTaskId: task.parentTaskId,
396
- preferredAgent: task.preferredAgent ?? defaults.agent,
397
- preferredModel: resolvedModel.model,
398
- preferredModelHint: [task.preferredModelHint, resolvedModel.warning].filter(Boolean).join(" ").trim() || undefined,
399
- preferredThinking: task.preferredThinking ?? defaults.thinking,
400
- fallbackModels: fallbackModels.length ? fallbackModels : undefined,
401
- dispatchPolicy: task.dispatchPolicy ?? defaults.dispatchPolicy,
402
- skills: task.skills,
403
- checklist: (task.checklist ?? []).map((item) => typeof item === "string" ? { text: item } : item),
404
- objective: task.objective,
405
- scope: task.scope,
406
- definitionOfDone: task.definitionOfDone,
407
- expectedArtifacts: task.expectedArtifacts,
408
- dependencies: task.dependencies,
409
- reviewCheckpoint: task.reviewCheckpoint,
410
- instructions: task.instructions,
411
- conversationId: task.conversationId,
412
- }));
413
- }
414
-
415
- return nextTasks;
416
- }
417
-
418
- async function applyProfileDefaultsToTasks(ctx: ExtensionContext, tasks: OrchestratorTask[]): Promise<OrchestratorTask[]> {
419
- const nextTasks: OrchestratorTask[] = [];
420
- for (const task of tasks) {
421
- const defaults = getProfileDefaults(activeProfile, task.role, task.stage);
422
- const fallbackModels = [
423
- ...(task.fallbackModels ?? []),
424
- ...(defaults.fallbackModels ?? []),
425
- ];
426
- const requestedModel = task.preferredModel ?? defaults.model;
427
- const resolvedModel = await resolvePreferredModel(ctx, requestedModel, fallbackModels);
428
- nextTasks.push({
429
- ...task,
430
- preferredAgent: task.preferredAgent ?? defaults.agent,
431
- preferredModel: resolvedModel.model,
432
- preferredModelHint: [task.preferredModelHint, resolvedModel.warning].filter(Boolean).join(" ").trim() || undefined,
433
- preferredThinking: task.preferredThinking ?? defaults.thinking,
434
- fallbackModels: fallbackModels.length ? fallbackModels : undefined,
435
- dispatchPolicy: task.dispatchPolicy ?? defaults.dispatchPolicy,
436
- });
437
- }
438
- return nextTasks;
439
- }
440
-
441
- // stripAnsi, visibleWidth, truncateToWidth, formatFooterNumber
442
- // are imported from "./shared"
443
-
444
- function installTakomiFooter(ctx: ExtensionContext, stateRef: { current: TakomiState }): void {
445
- ctx.ui.setFooter((tui, theme, footerData) => new TakomiFooterComponent(tui, theme, footerData, ctx, () => stateRef.current));
446
- return;
447
- ctx.ui.setFooter((_tui, theme, footerData) => ({
448
- invalidate() {},
449
- render(width: number): string[] {
450
- const state = stateRef.current;
451
- let input = 0;
452
- let output = 0;
453
- let cost = 0;
454
- for (const entry of ctx.sessionManager.getBranch()) {
455
- if (entry.type === "message" && entry.message.role === "assistant") {
456
- const message = entry.message as AssistantMessage;
457
- input += message.usage.input;
458
- output += message.usage.output;
459
- cost += message.usage.cost.total;
460
- }
461
- }
462
-
463
- const cwd = theme.fg("dim", ctx.cwd);
464
- const stats = theme.fg("dim", `↑${formatFooterNumber(input)} ↓${formatFooterNumber(output)} $${cost.toFixed(3)}`);
465
- const leftPad = " ".repeat(Math.max(1, width - visibleWidth(cwd) - visibleWidth(stats)));
466
- const topLine = truncateToWidth(cwd + leftPad + stats, width);
467
-
468
- const extensionStatuses = [...footerData.getExtensionStatuses().entries()]
469
- .filter(([key]) => key !== "takomi-runtime")
470
- .map(([, value]) => value)
471
- .filter(Boolean);
472
- const runtimeStatus = renderRuntimeStatus(theme, state);
473
- const left = [runtimeStatus, ...extensionStatuses].join(theme.fg("dim", " · "));
474
- const right = theme.fg("dim", ctx.model?.id || "no-model");
475
- const rightPad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
476
- const bottomLine = truncateToWidth(left + rightPad + right, width);
477
-
478
- return [topLine, bottomLine];
479
- },
480
- }));
481
- }
482
-
483
- // Mutable state ref so the footer closure always reads the latest state
484
- const footerStateRef: { current: TakomiState; installed: boolean } = { current: cloneState(DEFAULT_STATE), installed: false };
485
-
486
- async function refreshUi(ctx: ExtensionContext, state: TakomiState) {
487
- if (!ctx.hasUI) return;
488
- footerStateRef.current = state;
489
- ctx.ui.setStatus("takomi-runtime", renderRuntimeStatus(ctx.ui.theme, state));
490
- const widget = renderRuntimeWidget(ctx.ui.theme, state);
491
- ctx.ui.setWidget("takomi-runtime", widget.length > 0 ? widget : undefined);
492
- if (!footerStateRef.installed) {
493
- installTakomiFooter(ctx, footerStateRef);
494
- footerStateRef.installed = true;
495
- }
496
- }
497
-
498
- export default function takomiRuntime(pi: ExtensionAPI) {
499
- let state = cloneState(DEFAULT_STATE);
500
- const subagentController = getTakomiSubagentController();
501
- const contextPanel = new TakomiContextPanel();
502
- let runtimeCtx: ExtensionContext | undefined;
503
- const pendingSubagentEvents: TakomiSubagentRuntimeEvent[] = [];
504
-
505
- // Wire context panel events and commands (Alt+C, /takomi-context)
506
- wireContextPanel(pi, contextPanel);
507
-
508
- pi.events.on(TAKOMI_SUBAGENT_EVENT_CHANNEL, (payload) => {
509
- const event = payload as TakomiSubagentRuntimeEvent;
510
- if (!runtimeCtx) {
511
- pendingSubagentEvents.push(event);
512
- return;
513
- }
514
- void applySubagentRuntimeEvent(event, runtimeCtx);
515
- });
516
-
517
- function persistState() {
518
- pi.appendEntry(STATE_ENTRY, state);
519
- }
520
-
521
- function syncContextPanelState() {
522
- contextPanel.setRuntimeState({
523
- role: state.role,
524
- stage: state.stage,
525
- workflow: state.workflow,
526
- activeSessionId: state.activeSessionId,
527
- autoOrch: state.autoOrch,
528
- launchMode: state.launchMode,
529
- planMode: state.planMode,
530
- activeSubagent: activeSubagentLabel,
531
- });
532
- }
533
-
534
- async function applySubagentRuntimeEvent(event: TakomiSubagentRuntimeEvent, ctx: ExtensionContext): Promise<void> {
535
- if (event.type === "start") {
536
- activeSubagentLabel = `${event.state.agent}: ${event.state.taskLabel}`;
537
- syncContextPanelState();
538
- } else if ((event.type === "update" || event.type === "complete" || event.type === "block") && event.patch) {
539
- const model = event.patch.model ? ` @ ${event.patch.model}` : "";
540
- const thinking = event.patch.thinking ? ` (${event.patch.thinking})` : "";
541
- const label = event.patch.summary?.split(/\r?\n/).find(Boolean);
542
- if (label) activeSubagentLabel = `${label}${model}${thinking}`;
543
- syncContextPanelState();
544
- }
545
- switch (event.type) {
546
- case "start":
547
- await subagentController.start(ctx, event.state, event.runKey);
548
- break;
549
- case "update":
550
- await subagentController.update(ctx, event.patch, event.runKey);
551
- break;
552
- case "appendLog":
553
- await subagentController.appendLog(ctx, event.chunk, event.runKey);
554
- break;
555
- case "complete":
556
- await subagentController.complete(ctx, event.patch, event.runKey);
557
- break;
558
- case "block":
559
- await subagentController.block(ctx, event.patch, event.runKey);
560
- break;
561
- }
562
- }
563
-
564
- function flushPendingSubagentEvents(): void {
565
- if (!runtimeCtx || pendingSubagentEvents.length === 0) return;
566
- const queued = pendingSubagentEvents.splice(0, pendingSubagentEvents.length);
567
- for (const event of queued) {
568
- void applySubagentRuntimeEvent(event, runtimeCtx);
569
- }
570
- }
571
-
572
- async function updateState(ctx: ExtensionContext, mutator: () => void, message?: string | (() => string)) {
573
- mutator();
574
- persistState();
575
- syncContextPanelState();
576
- await refreshUi(ctx, state);
577
- const resolvedMessage = typeof message === "function" ? message() : message;
578
- if (resolvedMessage) ctx.ui.notify(resolvedMessage, "info");
579
- }
580
-
581
- async function syncBoardTaskRunState(
582
- ctx: ExtensionContext,
583
- task: Pick<OrchestratorTask, "conversationId" | "status" | "checklist">,
584
- summary?: string,
585
- ): Promise<void> {
586
- if (!task.conversationId) return;
587
- const patch: TakomiSubagentRunPatch = {
588
- conversationId: task.conversationId,
589
- boardTaskStatus: task.status,
590
- checklist: task.checklist,
591
- };
592
- if (summary) patch.summary = summary;
593
- await subagentController.update(ctx, patch, task.conversationId);
594
- }
595
-
596
- registerTakomiCommands(pi, {
597
- getState: () => state,
598
- updateState,
599
- setStageAndWorkflow: (stage, options) => setStageAndWorkflow(state, stage, options),
600
- hasGenesisArtifacts,
601
- subagentController,
602
- createPlanSession: async (ctx, title) => {
603
- const starter = createLifecycleStarterSession(title?.trim() || "Takomi Project");
604
- const session = buildSessionState(
605
- starter.sessionId,
606
- starter.title,
607
- await applyProfileDefaultsToTasks(ctx, starter.tasks),
608
- new Date(),
609
- { sessionIntent: starter.sessionIntent, lifecycle: starter.lifecycle },
610
- );
611
- const paths = await writeOrchestratorSession(ctx.cwd, session);
612
- await updateState(ctx, () => {
613
- state.enabled = true;
614
- state.autoOrch = true;
615
- state.planMode = true;
616
- state.activeSessionId = session.sessionId;
617
- state.stage = "genesis";
618
- state.workflow = "vibe-genesis";
619
- state.role = "orchestrator";
620
- });
621
- return `Takomi plan created session ${session.sessionId}\nMaster plan: ${paths.masterPlan}`;
622
- },
623
- resetRuntime: async (ctx) => {
624
- await updateState(ctx, () => {
625
- state = cloneState(DEFAULT_STATE);
626
- activeSubagentLabel = undefined;
627
- }, "Takomi runtime state reset");
628
- subagentController.reset(ctx);
629
- contextPanel.resetSession();
630
- contextPanel.show(ctx);
631
- },
632
- });
633
-
634
- pi.registerShortcut("alt+t", {
635
- description: "Toggle native tool result expansion",
636
- handler: async (ctx) => {
637
- const expanded = !ctx.ui.getToolsExpanded();
638
- ctx.ui.setToolsExpanded(expanded);
639
- ctx.ui.notify(`${expanded ? "Expanded" : "Collapsed"} native tool results.`, "info");
640
- },
641
- });
642
-
643
- pi.registerShortcut("alt+shift+t", {
644
- description: "Expand native tool results",
645
- handler: async (ctx) => {
646
- ctx.ui.setToolsExpanded(true);
647
- ctx.ui.notify("Expanded native tool results for Takomi subagent output.", "info");
648
- },
649
- });
650
-
651
- pi.registerShortcut("alt+n", {
652
- description: "Show native subagent navigation hint",
653
- handler: async (ctx) => {
654
- ctx.ui.notify("Native subagent results are shown inline by Pi; use the transcript/tool expansion instead of Takomi focus cycling.", "info");
655
- },
656
- });
657
-
658
- pi.registerShortcut("alt+p", {
659
- description: "Show native subagent navigation hint",
660
- handler: async (ctx) => {
661
- ctx.ui.notify("Native subagent results are shown inline by Pi; use the transcript/tool expansion instead of Takomi focus cycling.", "info");
662
- },
663
- });
664
-
665
- pi.registerTool({
666
- name: "takomi_workflow",
667
- label: "Takomi Workflow",
668
- description: "Return embedded Takomi workflow playbooks for genesis, design, and build.",
669
- promptSnippet: "Get embedded Takomi lifecycle playbooks without relying on external skill files.",
670
- parameters: Type.Object({
671
- workflow: Type.Optional(StringEnum(["vibe-genesis", "vibe-design", "vibe-build"] as const)),
672
- }),
673
- async execute(_toolCallId, params) {
674
- if (params.workflow) {
675
- const workflow = getWorkflowDefinition(params.workflow);
676
- return {
677
- content: [{ type: "text", text: `${workflow.title}\n\n${workflow.playbook}` }],
678
- details: workflow,
679
- };
680
- }
681
-
682
- const workflows = listWorkflowDefinitions();
683
- return {
684
- content: [{ type: "text", text: workflows.map((workflow) => `${workflow.id}: ${workflow.purpose}`).join("\n") }],
685
- details: undefined,
686
- };
687
- },
688
- });
689
-
690
- pi.registerTool({
691
- name: "takomi_board",
692
- label: "Takomi Board",
693
- description: "Create and manage lifecycle-aware Takomi orchestration session artifacts.",
694
- promptSnippet: "Create or expand a Genesis -> Design -> Build orchestration session only when the work is large enough to merit it.",
695
- promptGuidelines: [
696
- "Use this when you need a concrete orchestrator session directory and task artifacts on disk.",
697
- "A new session should normally begin Genesis-first, then expand Design and Build into as many tasks as the scope actually needs.",
698
- "If the request is small enough, do not force orchestration just because the tool exists.",
699
- "If a reviewed task needs more work, keep or reuse its conversationId so the same subagent can continue it.",
700
- ],
701
- parameters: Type.Object({
702
- action: StringEnum(["init_session", "expand_stage", "show_workflows", "show_session", "update_task", "redispatch_task", "review_and_redispatch", "dispatch_tasks"] as const),
703
- title: Type.Optional(Type.String()),
704
- sessionId: Type.Optional(Type.String()),
705
- taskId: Type.Optional(Type.String()),
706
- taskIds: Type.Optional(Type.Array(Type.String())),
707
- dispatchMode: Type.Optional(StringEnum(["single", "parallel", "chain"] as const)),
708
- agentScope: Type.Optional(StringEnum(["user", "project", "both"] as const)),
709
- confirmProjectAgents: Type.Optional(Type.Boolean()),
710
- stage: Type.Optional(StringEnum(["genesis", "design", "build"] as const)),
711
- status: Type.Optional(StringEnum(["pending", "in-progress", "completed", "blocked"] as const)),
712
- notes: Type.Optional(Type.String()),
713
- rerunInstructions: Type.Optional(Type.String()),
714
- confirmLaunch: Type.Optional(Type.Boolean()),
715
- previewOnly: Type.Optional(Type.Boolean()),
716
- preferredAgent: Type.Optional(Type.String()),
717
- preferredModel: Type.Optional(Type.String()),
718
- preferredThinking: Type.Optional(ThinkingSchema),
719
- includeReview: Type.Optional(Type.Boolean()),
720
- checklist: Type.Optional(Type.Array(Type.Union([
721
- Type.String(),
722
- Type.Object({ text: Type.String(), done: Type.Optional(Type.Boolean()) }),
723
- ]))),
724
- checklistUpdates: Type.Optional(Type.Array(Type.Object({
725
- text: Type.Optional(Type.String()),
726
- index: Type.Optional(Type.Number()),
727
- done: Type.Optional(Type.Boolean()),
728
- }))),
729
- tasks: Type.Optional(Type.Array(Type.Object({
730
- id: Type.Optional(Type.String()),
731
- title: Type.String(),
732
- role: StringEnum(["general", "orchestrator", "architect", "design", "code", "review"] as const),
733
- stage: Type.Optional(StringEnum(["genesis", "design", "build"] as const)),
734
- workflow: Type.Optional(StringEnum(["vibe-genesis", "vibe-design", "vibe-build"] as const)),
735
- parentTaskId: Type.Optional(Type.String()),
736
- preferredAgent: Type.Optional(Type.String()),
737
- preferredModel: Type.Optional(Type.String()),
738
- preferredModelHint: Type.Optional(Type.String()),
739
- preferredThinking: Type.Optional(ThinkingSchema),
740
- fallbackModels: Type.Optional(Type.Array(Type.String())),
741
- dispatchPolicy: Type.Optional(StringEnum(["direct", "subagent", "review-first"] as const)),
742
- skills: Type.Optional(Type.Array(Type.String())),
743
- checklist: Type.Optional(Type.Array(Type.Union([
744
- Type.String(),
745
- Type.Object({ text: Type.String(), done: Type.Optional(Type.Boolean()) }),
746
- ]))),
747
- objective: Type.Optional(Type.String()),
748
- scope: Type.Optional(Type.Array(Type.String())),
749
- definitionOfDone: Type.Optional(Type.Array(Type.String())),
750
- expectedArtifacts: Type.Optional(Type.Array(Type.String())),
751
- dependencies: Type.Optional(Type.Array(Type.String())),
752
- reviewCheckpoint: Type.Optional(Type.String()),
753
- instructions: Type.Optional(Type.Array(Type.String())),
754
- conversationId: Type.Optional(Type.String()),
755
- }))),
756
- }),
757
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
758
- if (params.action === "show_workflows") {
759
- const workflows = listWorkflowDefinitions();
760
- return {
761
- content: [{ type: "text", text: workflows.map((workflow) => `${workflow.id}: ${workflow.playbook}`).join("\n\n") }],
762
- details: { workflows },
763
- };
764
- }
765
-
766
- if (params.action === "show_session") {
767
- if (!params.sessionId) {
768
- return { content: [{ type: "text", text: "sessionId is required for show_session" }], details: {}, isError: true };
769
- }
770
- const paths = getSessionPaths(ctx.cwd, params.sessionId);
771
- const [masterPlan, stateJson] = await Promise.all([
772
- readFile(paths.masterPlan, "utf8").catch(() => "Master plan not found."),
773
- readFile(paths.stateFile, "utf8").catch(() => "{}"),
774
- ]);
775
- return {
776
- content: [{ type: "text", text: `${masterPlan}\n\n---\n\nMachine state\n\n\
777
- ${stateJson}` }],
778
- details: { paths, state: normalizeSessionState({ sessionId: params.sessionId, title: "Takomi Session", ...(JSON.parse(stateJson) as Partial<OrchestratorSessionState>) }) },
779
- };
780
- }
781
-
782
- if (params.action === "update_task") {
783
- if (!params.sessionId || !params.taskId) {
784
- return { content: [{ type: "text", text: "sessionId and taskId are required for update_task" }], details: {}, isError: true };
785
- }
786
- const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
787
- const idx = sessionState.tasks.findIndex((task) => task.id === params.taskId);
788
- if (idx === -1) {
789
- return { content: [{ type: "text", text: `Task ${params.taskId} not found in session ${params.sessionId}` }], details: {}, isError: true };
790
- }
791
- const current = sessionState.tasks[idx];
792
- const checklist = resolveChecklistState(current.checklist, params.checklist, params.checklistUpdates);
793
- const nextTask = {
794
- ...current,
795
- status: (params.status ?? current.status) as OrchestratorTaskStatus,
796
- notes: params.notes ?? current.notes,
797
- checklist,
798
- };
799
- if (params.status === "completed") {
800
- const completionGateError = getCompletionGateError(nextTask);
801
- if (completionGateError) {
802
- return {
803
- content: [{ type: "text", text: completionGateError }],
804
- details: {
805
- sessionId: params.sessionId,
806
- taskId: current.id,
807
- incompleteChecklistItems: getIncompleteChecklistItems(nextTask.checklist),
808
- checklist: nextTask.checklist,
809
- },
810
- isError: true,
811
- };
812
- }
813
- }
814
- sessionState.tasks[idx] = nextTask;
815
- const nextState = buildSessionState(
816
- sessionState.sessionId,
817
- sessionState.title,
818
- sessionState.tasks,
819
- new Date(),
820
- {
821
- sessionIntent: sessionState.sessionIntent,
822
- lifecycle: sessionState.lifecycle,
823
- },
824
- );
825
- const paths = await syncTaskArtifacts(ctx.cwd, nextState);
826
- await syncBoardTaskRunState(
827
- ctx,
828
- nextState.tasks[idx],
829
- nextTask.status === "completed"
830
- ? "Board task completed."
831
- : nextTask.status === "blocked"
832
- ? "Board task blocked."
833
- : undefined,
834
- );
835
- return {
836
- content: [{ type: "text", text: `Updated task ${params.taskId} in session ${params.sessionId}.\nStatus: ${nextState.tasks[idx].status}` }],
837
- details: { sessionId: params.sessionId, task: nextState.tasks[idx], paths, lifecycle: nextState.lifecycle },
838
- };
839
- }
840
-
841
- if (params.action === "dispatch_tasks") {
842
- if (!state.subagentsEnabled) {
843
- return {
844
- content: [{ type: "text", text: "Takomi subagents are disabled. Use /takomi subagents on before dispatching board tasks." }],
845
- details: { sessionId: params.sessionId, taskIds: params.taskIds },
846
- isError: true,
847
- };
848
- }
849
- if (!params.sessionId || !params.taskIds?.length) {
850
- return { content: [{ type: "text", text: "sessionId and taskIds are required for dispatch_tasks" }], details: {}, isError: true };
851
- }
852
- const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
853
- const selectedTasks = params.taskIds.map((id) => sessionState.tasks.find((task) => task.id === id));
854
- if (selectedTasks.some((task) => !task)) {
855
- return {
856
- content: [{ type: "text", text: `Some taskIds were not found in session ${params.sessionId}.` }],
857
- details: { requestedTaskIds: params.taskIds, availableTaskIds: sessionState.tasks.map((task) => task.id) },
858
- isError: true,
859
- };
860
- }
861
- const boardTasks = selectedTasks as OrchestratorTask[];
862
- const mode = params.dispatchMode ?? (boardTasks.length === 1 ? "single" : "parallel");
863
- const toolTasks = boardTasks.map((task) => ({
864
- agent: params.preferredAgent ?? resolveTaskAgent(task),
865
- task: task.notes || task.title,
866
- workflow: task.workflow,
867
- skills: task.skills,
868
- model: params.preferredModel ?? task.preferredModel,
869
- fallbackModels: task.fallbackModels,
870
- thinking: params.preferredThinking ?? task.preferredThinking,
871
- conversationId: task.conversationId ?? task.id,
872
- checklist: task.checklist,
873
- }));
874
- if (params.previewOnly || ((state.launchMode ?? activeProfile.launchMode) === "manual" && !params.confirmLaunch)) {
875
- return executeTakomiSubagentTool(pi, {
876
- ...(mode === "chain" ? { chain: toolTasks } : mode === "parallel" ? { tasks: toolTasks } : toolTasks[0]),
877
- previewOnly: true,
878
- agentScope: params.agentScope ?? "both",
879
- confirmProjectAgents: params.confirmProjectAgents,
880
- }, _signal, _onUpdate, ctx);
881
- }
882
- for (const task of boardTasks) task.status = "in-progress";
883
- let nextState = buildSessionState(sessionState.sessionId, sessionState.title, sessionState.tasks, new Date(), {
884
- sessionIntent: sessionState.sessionIntent,
885
- lifecycle: sessionState.lifecycle,
886
- });
887
- const paths = await syncTaskArtifacts(ctx.cwd, nextState);
888
- const toolResult = await executeTakomiSubagentTool(pi, {
889
- ...(mode === "chain" ? { chain: toolTasks } : mode === "parallel" ? { tasks: toolTasks } : toolTasks[0]),
890
- confirmLaunch: true,
891
- agentScope: params.agentScope ?? "both",
892
- confirmProjectAgents: params.confirmProjectAgents,
893
- }, _signal, _onUpdate, ctx);
894
- const results = (toolResult.details as { results?: TakomiDispatchResult[] }).results ?? [];
895
- for (let index = 0; index < boardTasks.length; index++) {
896
- const result = results[index];
897
- const task = boardTasks[index];
898
- if (!result) continue;
899
- task.conversationId = result.conversationId;
900
- task.notes = appendTaskNote(task.notes, "Model preflight", result.preflight);
901
- task.notes = appendTaskNote(task.notes, "Last dispatch output", result.output || result.stderr);
902
- if (result.code !== 0) task.status = "blocked";
903
- if (result.model) task.preferredModel = result.model;
904
- if (result.thinking) task.preferredThinking = result.thinking;
905
- }
906
- nextState = buildSessionState(sessionState.sessionId, sessionState.title, sessionState.tasks, new Date(), {
907
- sessionIntent: sessionState.sessionIntent,
908
- lifecycle: sessionState.lifecycle,
909
- });
910
- await syncTaskArtifacts(ctx.cwd, nextState);
911
- return {
912
- content: toolResult.content,
913
- details: { ...toolResult.details, sessionId: params.sessionId, paths, lifecycle: nextState.lifecycle, mode },
914
- isError: results.some((result) => result.code !== 0) || undefined,
915
- };
916
- }
917
-
918
- if (params.action === "redispatch_task" || params.action === "review_and_redispatch") {
919
- if (!state.subagentsEnabled) {
920
- return {
921
- content: [{ type: "text", text: "Takomi subagents are disabled. Use /takomi subagents on before redispatching." }],
922
- details: { sessionId: params.sessionId, taskId: params.taskId },
923
- isError: true,
924
- };
925
- }
926
- if (!params.sessionId || !params.taskId) {
927
- return { content: [{ type: "text", text: "sessionId and taskId are required for redispatch_task" }], details: {}, isError: true };
928
- }
929
- const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
930
- const task = sessionState.tasks.find((item) => item.id === params.taskId);
931
- if (!task) {
932
- return { content: [{ type: "text", text: `Task ${params.taskId} not found in session ${params.sessionId}` }], details: {}, isError: true };
933
- }
934
- const draftChecklist = resolveChecklistState(task.checklist, params.checklist, params.checklistUpdates);
935
- const agentName = params.preferredAgent ?? resolveTaskAgent(task);
936
- const draftModel = params.preferredModel ?? task.preferredModel;
937
- const draftThinking = params.preferredThinking ?? task.preferredThinking;
938
- const conversationId = task.conversationId ?? task.id;
939
- const launchMode = state.launchMode ?? activeProfile.launchMode ?? "auto";
940
- const plan = createTakomiDelegationPlan({
941
- source: "runtime-board",
942
- sessionId: params.sessionId,
943
- launchMode,
944
- profile: activeProfile,
945
- tasks: [{
946
- id: task.id,
947
- title: task.title,
948
- agent: agentName,
949
- task: params.rerunInstructions ?? task.notes ?? task.title,
950
- role: task.role,
951
- stage: task.stage,
952
- workflow: task.workflow,
953
- model: draftModel,
954
- fallbackModels: task.fallbackModels,
955
- thinking: draftThinking,
956
- conversationId,
957
- checklist: draftChecklist,
958
- dispatchPolicy: task.dispatchPolicy,
959
- review: params.includeReview,
960
- }],
961
- });
962
- if (params.previewOnly || (launchMode === "manual" && !params.confirmLaunch)) {
963
- return {
964
- content: [{ type: "text", text: renderTakomiDelegationPlan(plan) }],
965
- details: { sessionId: params.sessionId, taskId: task.id, plan },
966
- };
967
- }
968
-
969
- const agents: TakomiAgentConfig[] = discoverProjectAgents(ctx.cwd);
970
- const config = agents.find((agent: TakomiAgentConfig) => agent.name === agentName);
971
- if (!config) {
972
- return { content: [{ type: "text", text: `Preferred agent '${agentName}' not found.` }], details: { availableAgents: agents.map((agent: TakomiAgentConfig) => agent.name) }, isError: true };
973
- }
974
-
975
- task.status = "in-progress";
976
- task.checklist = draftChecklist;
977
- task.preferredAgent = agentName;
978
- task.preferredModel = draftModel;
979
- task.preferredThinking = draftThinking;
980
- if (params.action === "review_and_redispatch") {
981
- task.notes = appendTaskNote(task.notes, "Review feedback", params.notes);
982
- } else if (params.notes) {
983
- task.notes = params.notes;
984
- }
985
- let nextState = buildSessionState(
986
- sessionState.sessionId,
987
- sessionState.title,
988
- sessionState.tasks,
989
- new Date(),
990
- {
991
- sessionIntent: sessionState.sessionIntent,
992
- lifecycle: sessionState.lifecycle,
993
- },
994
- );
995
- const paths = await syncTaskArtifacts(ctx.cwd, nextState);
996
- task.conversationId = conversationId;
997
- const runKey = conversationId;
998
- const parentRunKey = task.parentTaskId
999
- ? (() => {
1000
- const parentTask = sessionState.tasks.find((item) => item.id === task.parentTaskId);
1001
- if (parentTask) return parentTask.conversationId ?? parentTask.id;
1002
- return subagentController.getKnownParentRunKey(task.parentTaskId);
1003
- })()
1004
- : undefined;
1005
-
1006
- const result = await dispatchTakomiSubagent(ctx, {
1007
- agent: config,
1008
- task: task.notes || task.title,
1009
- rootCwd: ctx.cwd,
1010
- workflow: task.workflow,
1011
- skills: task.skills,
1012
- model: task.preferredModel,
1013
- fallbackModels: task.fallbackModels,
1014
- thinking: task.preferredThinking,
1015
- conversationId,
1016
- checklist: task.checklist,
1017
- stage: task.stage,
1018
- taskLabel: `${task.id} - ${task.title}`,
1019
- parentTaskId: task.parentTaskId,
1020
- parentRunKey,
1021
- boardTaskStatus: task.status,
1022
- source: "runtime-board",
1023
- rerunInstructions: params.rerunInstructions,
1024
- }, _signal, {
1025
- emit: (event) => {
1026
- void applySubagentRuntimeEvent(event, ctx);
1027
- },
1028
- });
1029
-
1030
- task.notes = appendTaskNote(task.notes, "Model preflight", result.preflight);
1031
- if (result.model) task.preferredModel = result.model;
1032
- if (result.warning) {
1033
- task.notes = appendTaskNote(task.notes, "Model fallback", result.warning);
1034
- if (ctx.hasUI) ctx.ui.notify(result.warning, "warning");
1035
- }
1036
- if (result.thinking) task.preferredThinking = result.thinking;
1037
-
1038
- if (result.code !== 0) {
1039
- task.status = "blocked";
1040
- task.notes = appendTaskNote(task.notes, "Redispatch failure", result.stderr || result.output);
1041
- nextState = buildSessionState(
1042
- sessionState.sessionId,
1043
- sessionState.title,
1044
- sessionState.tasks,
1045
- new Date(),
1046
- {
1047
- sessionIntent: sessionState.sessionIntent,
1048
- lifecycle: sessionState.lifecycle,
1049
- },
1050
- );
1051
- await syncTaskArtifacts(ctx.cwd, nextState);
1052
- return {
1053
- content: [{ type: "text", text: `Redispatch failed for task ${task.id}.\n\n${result.stderr || result.output || "No output"}` }],
1054
- details: { sessionId: params.sessionId, task, paths, result },
1055
- isError: true,
1056
- };
1057
- }
1058
-
1059
- task.notes = appendTaskNote(task.notes, "Last redispatch output", result.output);
1060
- nextState = buildSessionState(
1061
- sessionState.sessionId,
1062
- sessionState.title,
1063
- sessionState.tasks,
1064
- new Date(),
1065
- {
1066
- sessionIntent: sessionState.sessionIntent,
1067
- lifecycle: sessionState.lifecycle,
1068
- },
1069
- );
1070
- await syncTaskArtifacts(ctx.cwd, nextState);
1071
- return {
1072
- content: [{ type: "text", text: `${result.preflight}\n\n${result.output || `Redispatched task ${task.id} to ${agentName}.`}` }],
1073
- details: { sessionId: params.sessionId, task, paths, lifecycle: nextState.lifecycle, agent: agentName, conversationId: task.conversationId, action: params.action, result },
1074
- };
1075
- }
1076
-
1077
- if (params.action === "expand_stage") {
1078
- if (!params.sessionId || !params.stage || !params.tasks?.length) {
1079
- return { content: [{ type: "text", text: "sessionId, stage, and at least one task are required for expand_stage" }], details: {}, isError: true };
1080
- }
1081
-
1082
- const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
1083
- const tasks = await materializeTasksFromInput(ctx, sessionState.tasks, params.tasks as IncomingTask[], params.stage);
1084
- let nextState = buildSessionState(
1085
- sessionState.sessionId,
1086
- sessionState.title,
1087
- tasks,
1088
- new Date(),
1089
- {
1090
- sessionIntent: sessionState.sessionIntent,
1091
- lifecycle: sessionState.lifecycle,
1092
- },
1093
- );
1094
- nextState = markStageExpanded(nextState, params.stage, params.notes);
1095
- const paths = await writeOrchestratorSession(ctx.cwd, nextState);
1096
- state.activeSessionId = nextState.sessionId;
1097
- persistState();
1098
- syncContextPanelState();
1099
-
1100
- return {
1101
- content: [{ type: "text", text: `Expanded ${params.stage} stage in session ${nextState.sessionId}.\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks)}` }],
1102
- details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode },
1103
- };
1104
- }
1105
-
1106
- const sessionId = params.sessionId || createSessionId();
1107
- const title = params.title || "Takomi Session";
1108
- const baseState = params.tasks?.length
1109
- ? buildSessionState(sessionId, title, [], new Date())
1110
- : createLifecycleStarterSession(title, { sessionId });
1111
- const tasks = params.tasks?.length
1112
- ? await materializeTasksFromInput(ctx, baseState.tasks, params.tasks as IncomingTask[], params.stage)
1113
- : await applyProfileDefaultsToTasks(ctx, baseState.tasks);
1114
- const nextState = buildSessionState(
1115
- baseState.sessionId,
1116
- baseState.title,
1117
- tasks,
1118
- new Date(),
1119
- {
1120
- sessionIntent: baseState.sessionIntent,
1121
- lifecycle: baseState.lifecycle,
1122
- },
1123
- );
1124
- const paths = await writeOrchestratorSession(ctx.cwd, nextState);
1125
- state.activeSessionId = nextState.sessionId;
1126
- state.role = "orchestrator";
1127
- state.stage = nextState.lifecycle.genesis.status === "completed" ? "build" : "genesis";
1128
- state.workflow = state.stage === "genesis" ? "vibe-genesis" : "vibe-build";
1129
- persistState();
1130
- syncContextPanelState();
1131
-
1132
- return {
1133
- content: [{ type: "text", text: `Created Takomi orchestrator session ${nextState.sessionId} in hybrid mode\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks) || "No tasks provided."}` }],
1134
- details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode },
1135
- };
1136
- },
1137
- });
1138
-
1139
- pi.on("input", async (event) => {
1140
- if (event.source === "extension") return { action: "continue" };
1141
-
1142
- const text = event.text.trim();
1143
- const lowered = text.toLowerCase();
1144
-
1145
- const routingUpdateMatch = text.match(/^update\s+(?:takomi\s+|our\s+)?(?:model\s+)?routing\s+(?:logic|policy|philosophy)\s*:?\s*([\s\S]*)$/i)
1146
- ?? text.match(/^set\s+(?:takomi\s+|our\s+)?(?:model\s+)?routing\s+(?:logic|policy|philosophy)\s*:?\s*([\s\S]*)$/i);
1147
- if (routingUpdateMatch) {
1148
- state.enabled = true;
1149
- try {
1150
- const result = await installTakomiRoutingPolicy(runtimeCtx?.cwd ?? process.cwd(), text);
1151
- const detected = result.detectedDefaults.length ? `\n\nDetected defaults:\n- ${result.detectedDefaults.join("\n- ")}` : "\n\nNo model names were auto-detected; saved policy only.";
1152
- 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.` };
1153
- } catch (error) {
1154
- return { action: "transform", text: `Takomi routing policy update failed: ${error instanceof Error ? error.message : String(error)}` };
1155
- }
1156
- }
1157
-
1158
- if (lowered === "use takomi") {
1159
- state.enabled = true;
1160
- return { action: "transform", text: "Use the Takomi runtime, identify the correct lifecycle stage, and proceed accordingly." };
1161
- }
1162
-
1163
- if (lowered.startsWith("use takomi ")) {
1164
- state.enabled = true;
1165
- const route = decideRoute(text.slice("use takomi ".length));
1166
- if (route.stage) setStageAndWorkflow(state, route.stage, { preserveRole: state.role === "orchestrator" && route.stage === "genesis" });
1167
- else if (route.role !== "general") state.role = route.role;
1168
- return { action: "transform", text: `Use the Takomi runtime for this request: ${text.slice("use takomi ".length)}` };
1169
- }
1170
-
1171
- if (/\bvibe genesis\b/i.test(text)) {
1172
- setStageAndWorkflow(state, "genesis", { preserveRole: state.role === "orchestrator" });
1173
- return { action: "transform", text };
1174
- }
1175
- if (/\bvibe design\b/i.test(text)) {
1176
- setStageAndWorkflow(state, "design");
1177
- return { action: "transform", text };
1178
- }
1179
- if (/\bvibe build\b/i.test(text)) {
1180
- setStageAndWorkflow(state, "build");
1181
- return { action: "transform", text };
1182
- }
1183
-
1184
- return { action: "continue" };
1185
- });
1186
-
1187
- pi.on("before_agent_start", async (event) => {
1188
- if (!state.enabled) return;
1189
-
1190
- let effectiveState = cloneState(state);
1191
- const runtimeCwd = typeof (event as { cwd?: string }).cwd === "string" ? (event as { cwd?: string }).cwd as string : process.cwd();
1192
- const genesisExists = await hasGenesisArtifacts(runtimeCwd);
1193
- const route = decideRoute(event.prompt);
1194
- if (state.autoOrch && shouldAutoRoute(event.prompt)) {
1195
- effectiveState.role = "orchestrator";
1196
- effectiveState.stage = genesisExists ? "build" : "genesis";
1197
- effectiveState.workflow = genesisExists ? "vibe-build" : "vibe-genesis";
1198
- }
1199
-
1200
- const shouldHonorRoute = route.stage || route.role !== "general" || route.sessionRecommendation !== "none";
1201
- if (shouldHonorRoute && route.stage) {
1202
- effectiveState.stage = route.stage;
1203
- effectiveState.workflow = route.workflow;
1204
- effectiveState.role = effectiveState.role === "orchestrator" && route.stage === "genesis" ? "orchestrator" : route.role;
1205
- } else if (shouldHonorRoute && route.role !== "general") {
1206
- effectiveState.role = route.role;
1207
- }
1208
-
1209
- let routingNote = route.reason;
1210
- const explicitLifecycleWaiver = /skip genesis|waive genesis|genesis complete|already have (a )?(prd|requirements)|design complete|jump straight to build/i.test(event.prompt);
1211
- const orchestrationActive = effectiveState.role === "orchestrator" || route.executionMode === "orchestrate";
1212
- if (!genesisExists && orchestrationActive && !explicitLifecycleWaiver) {
1213
- effectiveState.stage = "genesis";
1214
- effectiveState.workflow = "vibe-genesis";
1215
- routingNote = "Blank project detected; orchestrator remains in control and must honor Genesis → Design → Build.";
1216
- }
1217
-
1218
- const routingPolicy = await loadTakomiRoutingPolicy(runtimeCwd);
1219
- const modelPreflightContext = (() => {
1220
- try {
1221
- const available = typeof (runtimeCtx as { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } } | undefined)?.modelRegistry?.getAvailable === "function"
1222
- ? (runtimeCtx as { modelRegistry: { getAvailable: () => Array<{ provider?: string; id?: string; name?: string }> } }).modelRegistry.getAvailable()
1223
- : [];
1224
- if (!available.length) return "";
1225
- return `Available model context from Pi registry: ${available.map((m) => `${m.provider ? `${m.provider}/` : ""}${m.id ?? m.name ?? "unknown"}`).slice(0, 80).join(", ")}`;
1226
- } catch {
1227
- return "";
1228
- }
1229
- })();
1230
-
1231
- const parts = [
1232
- "Takomi runtime is active for this turn.",
1233
- rolePrompt(effectiveState.role),
1234
- effectiveState.planMode ? planPrompt() : "",
1235
- getInjectedPlaybook(effectiveState),
1236
- `Routing note: ${routingNote}`,
1237
- routingPolicy ? `Project Takomi model routing policy is active. Apply it when choosing parent/subagent models and escalation levels:\n\n${routingPolicy}` : "No project Takomi model routing policy file was found. Users can install one with `/takomi routing <policy>` or by saying `Update Takomi routing logic: \"\"\"...\"\"\"`.",
1238
- modelPreflightContext,
1239
- `Execution mode: ${route.executionMode}. Session recommendation: ${route.sessionRecommendation}.`,
1240
- `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.`,
1241
- !effectiveState.subagentsEnabled ? "Takomi subagents are disabled for this session. Do not call takomi_subagent, subagent, or redispatch board tasks until the user enables subagents." : "",
1242
- !genesisExists ? "Project foundation is missing or incomplete. Do not skip Genesis unless the user explicitly waives it." : "",
1243
- "Takomi is the default orchestration mindset here. Do not wait for the literal phrase 'use Takomi' before applying lifecycle judgment.",
1244
- "Task fan-out is flexible. Do not force exactly three tasks; decompose Genesis, Design, and Build work to fit the actual scope.",
1245
- "A new orchestration session should usually begin with one Genesis foundation task that creates or updates the required markdown artifacts, then expand later stages only when the scope justifies it.",
1246
- "If a follow-up request is small, one-shot it. If it is multi-part or large, create or expand an orchestration session instead of pretending it is a single task.",
1247
- "Before any Takomi subagent dispatch or model override, use the injected Pi model-registry context and project routing policy. Prefer provider-qualified model IDs. Do not run `pi --list-models` unless the registry context is missing or the user asks for a visible diagnostic.",
1248
- "When useful, state the current Takomi stage and the recommended next stage.",
1249
- effectiveState.stage === "build"
1250
- ? "For build orchestration, it is valid to dispatch tasks to specialist subagents, review them, and send fixes back to the same agent by reusing its conversation id."
1251
- : "",
1252
- ].filter(Boolean);
1253
-
1254
- return {
1255
- systemPrompt: `${event.systemPrompt}\n\n${parts.join("\n\n")}`,
1256
- };
1257
- });
1258
-
1259
- pi.on("session_start", async (_event, ctx) => {
1260
- runtimeCtx = ctx;
1261
- activeProfile = await loadTakomiProfile(ctx.cwd);
1262
- activeSubagentLabel = undefined;
1263
- subagentController.reset(ctx);
1264
- contextPanel.resetSession();
1265
- const entries = ctx.sessionManager.getEntries();
1266
- for (let i = entries.length - 1; i >= 0; i--) {
1267
- const entry = entries[i] as { type: string; customType?: string; data?: TakomiState };
1268
- if (entry.type === "custom" && entry.customType === STATE_ENTRY && entry.data) {
1269
- state = { ...DEFAULT_STATE, ...entry.data };
1270
- break;
1271
- }
1272
- }
1273
- if (!entries.some((entry) => {
1274
- const item = entry as { type: string; customType?: string };
1275
- return item.type === "custom" && item.customType === STATE_ENTRY;
1276
- })) {
1277
- state.autoOrch = activeProfile.autoOrchestrate;
1278
- state.launchMode = activeProfile.launchMode ?? (activeProfile.autoOrchestrate ? "auto" : "manual");
1279
- } else {
1280
- state.launchMode = state.launchMode ?? activeProfile.launchMode ?? "auto";
1281
- }
1282
-
1283
- syncContextPanelState();
1284
- await refreshUi(ctx, state);
1285
- contextPanel.show(ctx);
1286
- flushPendingSubagentEvents();
1287
- });
1288
- }
1
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { AssistantMessage } from "@mariozechner/pi-ai";
4
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
+ import { StringEnum } from "@mariozechner/pi-ai";
6
+ import { Type } from "typebox";
7
+ import {
8
+ buildSessionState,
9
+ createSessionId,
10
+ createLifecycleStarterSession,
11
+ createTask,
12
+ decideRoute,
13
+ getSessionPaths,
14
+ getNextTaskId,
15
+ getWorkflowDefinition,
16
+ listWorkflowDefinitions,
17
+ markStageExpanded,
18
+ normalizeSessionState,
19
+ renderMasterPlan,
20
+ renderTaskFile,
21
+ serializeSessionState,
22
+ slugifyTaskTitle,
23
+ type OrchestratorTask,
24
+ type OrchestratorSessionState,
25
+ type OrchestratorTaskStatus,
26
+ type TakomiDispatchPolicy,
27
+ type TakomiLaunchMode,
28
+ type TakomiProfile,
29
+ type TakomiRole,
30
+ type TakomiThinkingLevel,
31
+ type TakomiWorkflowId,
32
+ type VibeLifecycleStage,
33
+ } from "../../../src/pi-takomi-core";
34
+ import { discoverProjectAgents, type TakomiAgentConfig } from "../takomi-subagents/agents";
35
+ import { dispatchTakomiSubagent, type TakomiDispatchResult } from "../takomi-subagents/dispatch";
36
+ import { createTakomiDelegationPlan, renderTakomiDelegationPlan } from "../takomi-subagents/delegation-plan";
37
+ import { executeTakomiSubagentTool } from "../takomi-subagents/tool-runner";
38
+ import {
39
+ renderRuntimeStatus,
40
+ renderRuntimeWidget,
41
+ TakomiFooterComponent,
42
+ } from "./ui";
43
+ import { getTakomiSubagentController } from "./subagent-controller";
44
+ import {
45
+ TAKOMI_SUBAGENT_EVENT_CHANNEL,
46
+ type TakomiSubagentRunPatch,
47
+ type TakomiSubagentRuntimeEvent,
48
+ } from "./subagent-types";
49
+ import {
50
+ visibleWidth,
51
+ truncateToWidth,
52
+ formatFooterNumber,
53
+ buildTaskPrompt,
54
+ resolvePreferredModel,
55
+ } from "./shared";
56
+ import { TakomiContextPanel, wireContextPanel } from "./context-panel";
57
+ import { registerTakomiCommands } from "./commands";
58
+ import {
59
+ DEFAULT_TAKOMI_PROFILE,
60
+ getProfileDefaults,
61
+ loadTakomiProfile,
62
+ } from "./profile";
63
+ import { installTakomiRoutingPolicy, loadTakomiRoutingPolicy } from "./routing-policy";
64
+
65
+ type TakomiState = {
66
+ enabled: boolean;
67
+ autoOrch: boolean;
68
+ launchMode: TakomiLaunchMode;
69
+ planMode: boolean;
70
+ role: TakomiRole;
71
+ stage?: VibeLifecycleStage;
72
+ workflow?: TakomiWorkflowId;
73
+ activeSessionId?: string;
74
+ subagentsEnabled: boolean;
75
+ };
76
+
77
+ const DEFAULT_STATE: TakomiState = {
78
+ enabled: true,
79
+ autoOrch: true,
80
+ launchMode: "auto",
81
+ planMode: false,
82
+ role: "general",
83
+ subagentsEnabled: true,
84
+ };
85
+
86
+ const STATE_ENTRY = "takomi-runtime-state";
87
+
88
+ let activeProfile: TakomiProfile = DEFAULT_TAKOMI_PROFILE;
89
+ let activeSubagentLabel: string | undefined;
90
+
91
+ const ThinkingSchema = Type.Union([
92
+ Type.Literal("off"),
93
+ Type.Literal("minimal"),
94
+ Type.Literal("low"),
95
+ Type.Literal("medium"),
96
+ Type.Literal("high"),
97
+ Type.Literal("xhigh"),
98
+ ]);
99
+
100
+ function cloneState(state: TakomiState): TakomiState {
101
+ return { ...state };
102
+ }
103
+
104
+ function formatState(state: TakomiState): string {
105
+ return [
106
+ `Takomi ${state.enabled ? "on" : "off"}`,
107
+ `role=${state.role}`,
108
+ `stage=${state.stage ?? "-"}`,
109
+ `workflow=${state.workflow ?? "-"}`,
110
+ `autoOrch=${state.autoOrch ? "on" : "off"}`,
111
+ `launch=${state.launchMode}`,
112
+ `plan=${state.planMode ? "on" : "off"}`,
113
+ `subagents=${state.subagentsEnabled ? "on" : "off"}`,
114
+ state.activeSessionId ? `session=${state.activeSessionId}` : "",
115
+ ].filter(Boolean).join(" | ");
116
+ }
117
+
118
+ function setStageAndWorkflow(state: TakomiState, stage: VibeLifecycleStage, options?: { preserveRole?: boolean }) {
119
+ state.stage = stage;
120
+ state.workflow = stage === "genesis" ? "vibe-genesis" : stage === "design" ? "vibe-design" : "vibe-build";
121
+ if (!options?.preserveRole) {
122
+ state.role = stage === "design" ? "design" : stage === "build" ? "orchestrator" : "architect";
123
+ }
124
+ state.enabled = true;
125
+ }
126
+
127
+ function rolePrompt(role: TakomiRole): string {
128
+ switch (role) {
129
+ case "orchestrator":
130
+ return [
131
+ "You are operating in Takomi orchestrator mode.",
132
+ "Break work into tasks, delegate with specialist agents, review outputs, and route revisions intelligently.",
133
+ "When a task needs more work, you may send it back to the same agent using the same conversation continuity if that is most efficient.",
134
+ ].join("\n");
135
+ case "architect":
136
+ return [
137
+ "You are operating in Takomi architect mode.",
138
+ "Clarify scope, define acceptance criteria, and build the project foundation before design or implementation.",
139
+ ].join("\n");
140
+ case "design":
141
+ return [
142
+ "You are operating in Takomi design mode.",
143
+ "Translate genesis context into build-ready UX and visual direction.",
144
+ "Prefer Gemini or a similarly strong design-oriented model if available.",
145
+ ].join("\n");
146
+ case "code":
147
+ return [
148
+ "You are operating in Takomi code mode.",
149
+ "Implement directly, keep scope controlled, and verify after changes.",
150
+ ].join("\n");
151
+ case "review":
152
+ return [
153
+ "You are operating in Takomi review mode.",
154
+ "Focus on correctness, risk, omissions, and actionable review feedback.",
155
+ ].join("\n");
156
+ default:
157
+ return [
158
+ "You are operating in Takomi general mode.",
159
+ "Choose the correct lifecycle stage and specialist behavior based on the request.",
160
+ ].join("\n");
161
+ }
162
+ }
163
+
164
+ function planPrompt(): string {
165
+ return [
166
+ "Takomi planning mode is active.",
167
+ "Before major implementation, produce a short numbered plan.",
168
+ "If the request is broad, explicitly identify whether the user is in genesis, design, or build.",
169
+ ].join("\n");
170
+ }
171
+
172
+ function getInjectedPlaybook(state: TakomiState): string | undefined {
173
+ if (!state.workflow) return undefined;
174
+ const workflow = getWorkflowDefinition(state.workflow);
175
+ return [
176
+ `${workflow.title} is the active Takomi workflow.`,
177
+ workflow.purpose,
178
+ workflow.preferredModelHint ?? "",
179
+ workflow.playbook,
180
+ workflow.nextStage ? `After this stage, recommend ${workflow.nextStage}.` : "",
181
+ ].filter(Boolean).join("\n\n");
182
+ }
183
+
184
+ function shouldAutoRoute(text: string): boolean {
185
+ const lowered = text.toLowerCase();
186
+ const broadSignal = ["use takomi", "orchestrate", "plan and build", "full workflow", "break this down", "coordinate"].some((signal) => lowered.includes(signal));
187
+ const multiClause = (lowered.match(/\b(and|then|also|after|while)\b/g) ?? []).length >= 2;
188
+ return broadSignal || (lowered.length > 220 && multiClause);
189
+ }
190
+
191
+ function buildTaskRows(tasks: OrchestratorTask[]): string {
192
+ return tasks.map((task) => `${task.id}: ${task.stage ?? "-"} | ${task.title} [${task.status}] -> ${task.preferredAgent ?? task.role}${task.conversationId ? ` (${task.conversationId})` : ""}${task.workflow ? ` | workflow=${task.workflow}` : ""}${task.preferredModel ? ` | model=${task.preferredModel}` : ""}${task.preferredThinking ? ` | thinking=${task.preferredThinking}` : ""}${task.dispatchPolicy ? ` | dispatch=${task.dispatchPolicy}` : ""}${task.skills?.length ? ` | skills=${task.skills.join(",")}` : ""}`).join("\n");
193
+ }
194
+
195
+ function resolveTaskAgent(task: OrchestratorTask): string {
196
+ return task.preferredAgent ?? (task.role === "code" ? "coder" : task.role === "design" ? "designer" : task.role === "architect" ? "architect" : task.role === "review" ? "reviewer" : "orchestrator");
197
+ }
198
+
199
+ function appendTaskNote(existing: string | undefined, heading: string, body?: string): string {
200
+ if (!body?.trim()) return existing ?? "";
201
+ return [existing, "", `${heading}:`, body.trim()].filter(Boolean).join("\n").trim();
202
+ }
203
+
204
+ function applyChecklistUpdates(
205
+ current: OrchestratorTask["checklist"],
206
+ updates?: Array<{ text?: string; index?: number; done?: boolean }>,
207
+ ): OrchestratorTask["checklist"] {
208
+ if (!current?.length || !updates?.length) return current;
209
+ const next = current.map((item: NonNullable<OrchestratorTask["checklist"]>[number]) => ({ ...item }));
210
+ for (const update of updates) {
211
+ const idx = typeof update.index === "number"
212
+ ? update.index
213
+ : typeof update.text === "string"
214
+ ? next.findIndex((item: NonNullable<OrchestratorTask["checklist"]>[number]) => item.text === update.text)
215
+ : -1;
216
+ if (idx >= 0 && next[idx]) next[idx] = { ...next[idx], done: update.done ?? next[idx].done };
217
+ }
218
+ return next;
219
+ }
220
+
221
+ function normalizeChecklistInput(
222
+ checklist?: Array<string | { text: string; done?: boolean }>,
223
+ ): OrchestratorTask["checklist"] {
224
+ if (!checklist?.length) return undefined;
225
+ return checklist.map((item) => typeof item === "string" ? { text: item, done: false } : { text: item.text, done: item.done ?? false });
226
+ }
227
+
228
+ function resolveChecklistState(
229
+ current: OrchestratorTask["checklist"],
230
+ nextChecklist?: Array<string | { text: string; done?: boolean }>,
231
+ updates?: Array<{ text?: string; index?: number; done?: boolean }>,
232
+ ): OrchestratorTask["checklist"] {
233
+ const baseChecklist = nextChecklist ? normalizeChecklistInput(nextChecklist) : current;
234
+ return applyChecklistUpdates(baseChecklist, updates);
235
+ }
236
+
237
+ function getIncompleteChecklistItems(checklist?: OrchestratorTask["checklist"]): string[] {
238
+ return (checklist ?? [])
239
+ .filter((item) => !item.done)
240
+ .map((item) => item.text);
241
+ }
242
+
243
+ function getCompletionGateError(task: Pick<OrchestratorTask, "id" | "title" | "checklist">): string | undefined {
244
+ if (!task.checklist?.length) {
245
+ return `Task ${task.id} cannot be marked completed until it has a checklist.`;
246
+ }
247
+ const incompleteItems = getIncompleteChecklistItems(task.checklist);
248
+ if (incompleteItems.length === 0) return undefined;
249
+ return [
250
+ `Task ${task.id} cannot be marked completed until every checklist item is done.`,
251
+ "",
252
+ "Incomplete checklist items:",
253
+ ...incompleteItems.map((item) => `- ${item}`),
254
+ ].join("\n");
255
+ }
256
+
257
+ function getTaskFolder(paths: ReturnType<typeof getSessionPaths>, status: OrchestratorTask["status"]) {
258
+ switch (status) {
259
+ case "in-progress":
260
+ return paths.inProgress;
261
+ case "completed":
262
+ return paths.completed;
263
+ case "blocked":
264
+ return paths.blocked;
265
+ case "pending":
266
+ default:
267
+ return paths.pending;
268
+ }
269
+ }
270
+
271
+ function getTaskFileName(task: OrchestratorTask): string {
272
+ return `${task.id}_${slugifyTaskTitle(task.title)}.task.md`;
273
+ }
274
+
275
+ function buildSubagentTaskPrompt(task: OrchestratorTask, extraInstructions?: string): string {
276
+ return buildTaskPrompt({
277
+ task: extraInstructions?.trim() || task.notes || task.title,
278
+ workflow: task.workflow,
279
+ skills: task.skills,
280
+ checklist: task.checklist,
281
+ stage: task.stage,
282
+ });
283
+ }
284
+
285
+ async function hasGenesisArtifacts(cwd: string): Promise<boolean> {
286
+ try {
287
+ await readFile(path.join(cwd, "docs", "Project_Requirements.md"), "utf8");
288
+ await readFile(path.join(cwd, "docs", "Coding_Guidelines.md"), "utf8");
289
+ const issues = await readdir(path.join(cwd, "docs", "issues"));
290
+ return issues.some((entry) => entry.endsWith(".md"));
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+
296
+ async function loadSessionState(cwd: string, sessionId: string): Promise<{ state: OrchestratorSessionState; paths: ReturnType<typeof getSessionPaths> }> {
297
+ const paths = getSessionPaths(cwd, sessionId);
298
+ const raw = await readFile(paths.stateFile, "utf8");
299
+ const parsed = JSON.parse(raw) as Partial<OrchestratorSessionState>;
300
+ const state = normalizeSessionState({
301
+ sessionId,
302
+ title: parsed.title ?? "Takomi Session",
303
+ ...parsed,
304
+ });
305
+ return { state, paths };
306
+ }
307
+
308
+ async function syncTaskArtifacts(cwd: string, session: OrchestratorSessionState) {
309
+ const normalizedState = normalizeSessionState(session);
310
+ const paths = getSessionPaths(cwd, normalizedState.sessionId);
311
+ await mkdir(paths.pending, { recursive: true });
312
+ await mkdir(paths.inProgress, { recursive: true });
313
+ await mkdir(paths.completed, { recursive: true });
314
+ await mkdir(paths.blocked, { recursive: true });
315
+ await mkdir(paths.stateDir, { recursive: true });
316
+ await writeFile(paths.masterPlan, renderMasterPlan(normalizedState), "utf8");
317
+ await writeFile(paths.summary, [
318
+ `# Orchestrator Summary: ${normalizedState.title}`,
319
+ "",
320
+ `- Session ID: ${normalizedState.sessionId}`,
321
+ `- Human docs: ${paths.root}`,
322
+ `- Machine state: ${paths.stateFile}`,
323
+ `- Runtime mode: ${normalizedState.mode}`,
324
+ `- Session intent: ${normalizedState.sessionIntent ?? "full-project"}`,
325
+ ].join("\n"), "utf8");
326
+ await writeFile(paths.stateFile, serializeSessionState(normalizedState), "utf8");
327
+
328
+ for (const folder of [paths.pending, paths.inProgress, paths.completed, paths.blocked]) {
329
+ const entries = await readdir(folder).catch(() => [] as string[]);
330
+ for (const entry of entries) {
331
+ if (entry.endsWith(".task.md")) {
332
+ await rm(path.join(folder, entry), { force: true });
333
+ }
334
+ }
335
+ }
336
+
337
+ for (const task of normalizedState.tasks) {
338
+ const filePath = path.join(getTaskFolder(paths, task.status), getTaskFileName(task));
339
+ await writeFile(filePath, renderTaskFile(task, `Parent session: ${normalizedState.sessionId}\n\nTask title: ${task.title}`), "utf8");
340
+ }
341
+
342
+ return paths;
343
+ }
344
+
345
+ async function writeOrchestratorSession(cwd: string, session: OrchestratorSessionState) {
346
+ return syncTaskArtifacts(cwd, session);
347
+ }
348
+
349
+ type IncomingTask = {
350
+ id?: string;
351
+ title: string;
352
+ role: TakomiRole;
353
+ stage?: VibeLifecycleStage;
354
+ workflow?: TakomiWorkflowId;
355
+ parentTaskId?: string;
356
+ preferredAgent?: string;
357
+ preferredModel?: string;
358
+ preferredModelHint?: string;
359
+ preferredThinking?: TakomiThinkingLevel;
360
+ fallbackModels?: string[];
361
+ dispatchPolicy?: TakomiDispatchPolicy;
362
+ skills?: string[];
363
+ checklist?: Array<string | { text: string; done?: boolean }>;
364
+ objective?: string;
365
+ scope?: string[];
366
+ definitionOfDone?: string[];
367
+ expectedArtifacts?: string[];
368
+ dependencies?: string[];
369
+ reviewCheckpoint?: string;
370
+ instructions?: string[];
371
+ conversationId?: string;
372
+ };
373
+
374
+ async function materializeTasksFromInput(
375
+ ctx: ExtensionContext,
376
+ currentTasks: OrchestratorTask[],
377
+ incoming: IncomingTask[],
378
+ stageOverride?: VibeLifecycleStage,
379
+ ): Promise<OrchestratorTask[]> {
380
+ const nextTasks = [...currentTasks];
381
+
382
+ for (const task of incoming) {
383
+ const stage = task.stage ?? stageOverride;
384
+ const defaults = getProfileDefaults(activeProfile, task.role, stage);
385
+ const fallbackModels = [
386
+ ...(task.fallbackModels ?? []),
387
+ ...(defaults.fallbackModels ?? []),
388
+ ];
389
+ const requestedModel = task.preferredModel ?? defaults.model;
390
+ const resolvedModel = await resolvePreferredModel(ctx, requestedModel, fallbackModels);
391
+ const id = task.id ?? getNextTaskId(nextTasks);
392
+ nextTasks.push(createTask(id, task.title, task.role, {
393
+ stage,
394
+ workflow: task.workflow,
395
+ parentTaskId: task.parentTaskId,
396
+ preferredAgent: task.preferredAgent ?? defaults.agent,
397
+ preferredModel: resolvedModel.model,
398
+ preferredModelHint: [task.preferredModelHint, resolvedModel.warning].filter(Boolean).join(" ").trim() || undefined,
399
+ preferredThinking: task.preferredThinking ?? defaults.thinking,
400
+ fallbackModels: fallbackModels.length ? fallbackModels : undefined,
401
+ dispatchPolicy: task.dispatchPolicy ?? defaults.dispatchPolicy,
402
+ skills: task.skills,
403
+ checklist: (task.checklist ?? []).map((item) => typeof item === "string" ? { text: item } : item),
404
+ objective: task.objective,
405
+ scope: task.scope,
406
+ definitionOfDone: task.definitionOfDone,
407
+ expectedArtifacts: task.expectedArtifacts,
408
+ dependencies: task.dependencies,
409
+ reviewCheckpoint: task.reviewCheckpoint,
410
+ instructions: task.instructions,
411
+ conversationId: task.conversationId,
412
+ }));
413
+ }
414
+
415
+ return nextTasks;
416
+ }
417
+
418
+ async function applyProfileDefaultsToTasks(ctx: ExtensionContext, tasks: OrchestratorTask[]): Promise<OrchestratorTask[]> {
419
+ const nextTasks: OrchestratorTask[] = [];
420
+ for (const task of tasks) {
421
+ const defaults = getProfileDefaults(activeProfile, task.role, task.stage);
422
+ const fallbackModels = [
423
+ ...(task.fallbackModels ?? []),
424
+ ...(defaults.fallbackModels ?? []),
425
+ ];
426
+ const requestedModel = task.preferredModel ?? defaults.model;
427
+ const resolvedModel = await resolvePreferredModel(ctx, requestedModel, fallbackModels);
428
+ nextTasks.push({
429
+ ...task,
430
+ preferredAgent: task.preferredAgent ?? defaults.agent,
431
+ preferredModel: resolvedModel.model,
432
+ preferredModelHint: [task.preferredModelHint, resolvedModel.warning].filter(Boolean).join(" ").trim() || undefined,
433
+ preferredThinking: task.preferredThinking ?? defaults.thinking,
434
+ fallbackModels: fallbackModels.length ? fallbackModels : undefined,
435
+ dispatchPolicy: task.dispatchPolicy ?? defaults.dispatchPolicy,
436
+ });
437
+ }
438
+ return nextTasks;
439
+ }
440
+
441
+ // stripAnsi, visibleWidth, truncateToWidth, formatFooterNumber
442
+ // are imported from "./shared"
443
+
444
+ function installTakomiFooter(ctx: ExtensionContext, stateRef: { current: TakomiState }): void {
445
+ ctx.ui.setFooter((tui, theme, footerData) => new TakomiFooterComponent(tui, theme, footerData, ctx, () => stateRef.current));
446
+ return;
447
+ ctx.ui.setFooter((_tui, theme, footerData) => ({
448
+ invalidate() {},
449
+ render(width: number): string[] {
450
+ const state = stateRef.current;
451
+ let input = 0;
452
+ let output = 0;
453
+ let cost = 0;
454
+ for (const entry of ctx.sessionManager.getBranch()) {
455
+ if (entry.type === "message" && entry.message.role === "assistant") {
456
+ const message = entry.message as AssistantMessage;
457
+ input += message.usage.input;
458
+ output += message.usage.output;
459
+ cost += message.usage.cost.total;
460
+ }
461
+ }
462
+
463
+ const cwd = theme.fg("dim", ctx.cwd);
464
+ const stats = theme.fg("dim", `↑${formatFooterNumber(input)} ↓${formatFooterNumber(output)} $${cost.toFixed(3)}`);
465
+ const leftPad = " ".repeat(Math.max(1, width - visibleWidth(cwd) - visibleWidth(stats)));
466
+ const topLine = truncateToWidth(cwd + leftPad + stats, width);
467
+
468
+ const extensionStatuses = [...footerData.getExtensionStatuses().entries()]
469
+ .filter(([key]) => key !== "takomi-runtime")
470
+ .map(([, value]) => value)
471
+ .filter(Boolean);
472
+ const runtimeStatus = renderRuntimeStatus(theme, state);
473
+ const left = [runtimeStatus, ...extensionStatuses].join(theme.fg("dim", " · "));
474
+ const right = theme.fg("dim", ctx.model?.id || "no-model");
475
+ const rightPad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
476
+ const bottomLine = truncateToWidth(left + rightPad + right, width);
477
+
478
+ return [topLine, bottomLine];
479
+ },
480
+ }));
481
+ }
482
+
483
+ // Mutable state ref so the footer closure always reads the latest state
484
+ const footerStateRef: { current: TakomiState; installed: boolean } = { current: cloneState(DEFAULT_STATE), installed: false };
485
+
486
+ async function refreshUi(ctx: ExtensionContext, state: TakomiState) {
487
+ if (!ctx.hasUI) return;
488
+ footerStateRef.current = state;
489
+ ctx.ui.setStatus("takomi-runtime", renderRuntimeStatus(ctx.ui.theme, state));
490
+ const widget = renderRuntimeWidget(ctx.ui.theme, state);
491
+ ctx.ui.setWidget("takomi-runtime", widget.length > 0 ? widget : undefined);
492
+ if (!footerStateRef.installed) {
493
+ installTakomiFooter(ctx, footerStateRef);
494
+ footerStateRef.installed = true;
495
+ }
496
+ }
497
+
498
+ export default function takomiRuntime(pi: ExtensionAPI) {
499
+ let state = cloneState(DEFAULT_STATE);
500
+ const subagentController = getTakomiSubagentController();
501
+ const contextPanel = new TakomiContextPanel();
502
+ let runtimeCtx: ExtensionContext | undefined;
503
+ const pendingSubagentEvents: TakomiSubagentRuntimeEvent[] = [];
504
+
505
+ // Wire context panel events and commands (Alt+C, /takomi-context)
506
+ wireContextPanel(pi, contextPanel);
507
+
508
+ pi.events.on(TAKOMI_SUBAGENT_EVENT_CHANNEL, (payload) => {
509
+ const event = payload as TakomiSubagentRuntimeEvent;
510
+ if (!runtimeCtx) {
511
+ pendingSubagentEvents.push(event);
512
+ return;
513
+ }
514
+ void applySubagentRuntimeEvent(event, runtimeCtx);
515
+ });
516
+
517
+ function persistState() {
518
+ pi.appendEntry(STATE_ENTRY, state);
519
+ }
520
+
521
+ function syncContextPanelState() {
522
+ contextPanel.setRuntimeState({
523
+ role: state.role,
524
+ stage: state.stage,
525
+ workflow: state.workflow,
526
+ activeSessionId: state.activeSessionId,
527
+ autoOrch: state.autoOrch,
528
+ launchMode: state.launchMode,
529
+ planMode: state.planMode,
530
+ activeSubagent: activeSubagentLabel,
531
+ });
532
+ }
533
+
534
+ async function applySubagentRuntimeEvent(event: TakomiSubagentRuntimeEvent, ctx: ExtensionContext): Promise<void> {
535
+ if (event.type === "start") {
536
+ activeSubagentLabel = `${event.state.agent}: ${event.state.taskLabel}`;
537
+ syncContextPanelState();
538
+ } else if ((event.type === "update" || event.type === "complete" || event.type === "block") && event.patch) {
539
+ const model = event.patch.model ? ` @ ${event.patch.model}` : "";
540
+ const thinking = event.patch.thinking ? ` (${event.patch.thinking})` : "";
541
+ const label = event.patch.summary?.split(/\r?\n/).find(Boolean);
542
+ if (label) activeSubagentLabel = `${label}${model}${thinking}`;
543
+ syncContextPanelState();
544
+ }
545
+ switch (event.type) {
546
+ case "start":
547
+ await subagentController.start(ctx, event.state, event.runKey);
548
+ break;
549
+ case "update":
550
+ await subagentController.update(ctx, event.patch, event.runKey);
551
+ break;
552
+ case "appendLog":
553
+ await subagentController.appendLog(ctx, event.chunk, event.runKey);
554
+ break;
555
+ case "complete":
556
+ await subagentController.complete(ctx, event.patch, event.runKey);
557
+ break;
558
+ case "block":
559
+ await subagentController.block(ctx, event.patch, event.runKey);
560
+ break;
561
+ }
562
+ }
563
+
564
+ function flushPendingSubagentEvents(): void {
565
+ if (!runtimeCtx || pendingSubagentEvents.length === 0) return;
566
+ const queued = pendingSubagentEvents.splice(0, pendingSubagentEvents.length);
567
+ for (const event of queued) {
568
+ void applySubagentRuntimeEvent(event, runtimeCtx);
569
+ }
570
+ }
571
+
572
+ async function updateState(ctx: ExtensionContext, mutator: () => void, message?: string | (() => string)) {
573
+ mutator();
574
+ persistState();
575
+ syncContextPanelState();
576
+ await refreshUi(ctx, state);
577
+ const resolvedMessage = typeof message === "function" ? message() : message;
578
+ if (resolvedMessage) ctx.ui.notify(resolvedMessage, "info");
579
+ }
580
+
581
+ async function syncBoardTaskRunState(
582
+ ctx: ExtensionContext,
583
+ task: Pick<OrchestratorTask, "conversationId" | "status" | "checklist">,
584
+ summary?: string,
585
+ ): Promise<void> {
586
+ if (!task.conversationId) return;
587
+ const patch: TakomiSubagentRunPatch = {
588
+ conversationId: task.conversationId,
589
+ boardTaskStatus: task.status,
590
+ checklist: task.checklist,
591
+ };
592
+ if (summary) patch.summary = summary;
593
+ await subagentController.update(ctx, patch, task.conversationId);
594
+ }
595
+
596
+ registerTakomiCommands(pi, {
597
+ getState: () => state,
598
+ updateState,
599
+ setStageAndWorkflow: (stage, options) => setStageAndWorkflow(state, stage, options),
600
+ hasGenesisArtifacts,
601
+ subagentController,
602
+ createPlanSession: async (ctx, title) => {
603
+ const starter = createLifecycleStarterSession(title?.trim() || "Takomi Project");
604
+ const session = buildSessionState(
605
+ starter.sessionId,
606
+ starter.title,
607
+ await applyProfileDefaultsToTasks(ctx, starter.tasks),
608
+ new Date(),
609
+ { sessionIntent: starter.sessionIntent, lifecycle: starter.lifecycle },
610
+ );
611
+ const paths = await writeOrchestratorSession(ctx.cwd, session);
612
+ await updateState(ctx, () => {
613
+ state.enabled = true;
614
+ state.autoOrch = true;
615
+ state.planMode = true;
616
+ state.activeSessionId = session.sessionId;
617
+ state.stage = "genesis";
618
+ state.workflow = "vibe-genesis";
619
+ state.role = "orchestrator";
620
+ });
621
+ return `Takomi plan created session ${session.sessionId}\nMaster plan: ${paths.masterPlan}`;
622
+ },
623
+ resetRuntime: async (ctx) => {
624
+ await updateState(ctx, () => {
625
+ state = cloneState(DEFAULT_STATE);
626
+ activeSubagentLabel = undefined;
627
+ }, "Takomi runtime state reset");
628
+ subagentController.reset(ctx);
629
+ contextPanel.resetSession();
630
+ contextPanel.show(ctx);
631
+ },
632
+ });
633
+
634
+ pi.registerShortcut("alt+t", {
635
+ description: "Toggle native tool result expansion",
636
+ handler: async (ctx) => {
637
+ const expanded = !ctx.ui.getToolsExpanded();
638
+ ctx.ui.setToolsExpanded(expanded);
639
+ ctx.ui.notify(`${expanded ? "Expanded" : "Collapsed"} native tool results.`, "info");
640
+ },
641
+ });
642
+
643
+ pi.registerShortcut("alt+shift+t", {
644
+ description: "Expand native tool results",
645
+ handler: async (ctx) => {
646
+ ctx.ui.setToolsExpanded(true);
647
+ ctx.ui.notify("Expanded native tool results for Takomi subagent output.", "info");
648
+ },
649
+ });
650
+
651
+ pi.registerShortcut("alt+n", {
652
+ description: "Show native subagent navigation hint",
653
+ handler: async (ctx) => {
654
+ ctx.ui.notify("Native subagent results are shown inline by Pi; use the transcript/tool expansion instead of Takomi focus cycling.", "info");
655
+ },
656
+ });
657
+
658
+ pi.registerShortcut("alt+p", {
659
+ description: "Show native subagent navigation hint",
660
+ handler: async (ctx) => {
661
+ ctx.ui.notify("Native subagent results are shown inline by Pi; use the transcript/tool expansion instead of Takomi focus cycling.", "info");
662
+ },
663
+ });
664
+
665
+ pi.registerTool({
666
+ name: "takomi_workflow",
667
+ label: "Takomi Workflow",
668
+ description: "Return embedded Takomi workflow playbooks for genesis, design, and build.",
669
+ promptSnippet: "Get embedded Takomi lifecycle playbooks without relying on external skill files.",
670
+ parameters: Type.Object({
671
+ workflow: Type.Optional(StringEnum(["vibe-genesis", "vibe-design", "vibe-build"] as const)),
672
+ }),
673
+ async execute(_toolCallId, params) {
674
+ if (params.workflow) {
675
+ const workflow = getWorkflowDefinition(params.workflow);
676
+ return {
677
+ content: [{ type: "text", text: `${workflow.title}\n\n${workflow.playbook}` }],
678
+ details: workflow,
679
+ };
680
+ }
681
+
682
+ const workflows = listWorkflowDefinitions();
683
+ return {
684
+ content: [{ type: "text", text: workflows.map((workflow) => `${workflow.id}: ${workflow.purpose}`).join("\n") }],
685
+ details: undefined,
686
+ };
687
+ },
688
+ });
689
+
690
+ pi.registerTool({
691
+ name: "takomi_board",
692
+ label: "Takomi Board",
693
+ description: "Create and manage lifecycle-aware Takomi orchestration session artifacts.",
694
+ promptSnippet: "Create or expand a Genesis -> Design -> Build orchestration session only when the work is large enough to merit it.",
695
+ promptGuidelines: [
696
+ "Use this when you need a concrete orchestrator session directory and task artifacts on disk.",
697
+ "A new session should normally begin Genesis-first, then expand Design and Build into as many tasks as the scope actually needs.",
698
+ "If the request is small enough, do not force orchestration just because the tool exists.",
699
+ "If a reviewed task needs more work, keep or reuse its conversationId so the same subagent can continue it.",
700
+ ],
701
+ parameters: Type.Object({
702
+ action: StringEnum(["init_session", "expand_stage", "show_workflows", "show_session", "update_task", "redispatch_task", "review_and_redispatch", "dispatch_tasks"] as const),
703
+ title: Type.Optional(Type.String()),
704
+ sessionId: Type.Optional(Type.String()),
705
+ taskId: Type.Optional(Type.String()),
706
+ taskIds: Type.Optional(Type.Array(Type.String())),
707
+ dispatchMode: Type.Optional(StringEnum(["single", "parallel", "chain"] as const)),
708
+ agentScope: Type.Optional(StringEnum(["user", "project", "both"] as const)),
709
+ confirmProjectAgents: Type.Optional(Type.Boolean()),
710
+ stage: Type.Optional(StringEnum(["genesis", "design", "build"] as const)),
711
+ status: Type.Optional(StringEnum(["pending", "in-progress", "completed", "blocked"] as const)),
712
+ notes: Type.Optional(Type.String()),
713
+ rerunInstructions: Type.Optional(Type.String()),
714
+ confirmLaunch: Type.Optional(Type.Boolean()),
715
+ previewOnly: Type.Optional(Type.Boolean()),
716
+ preferredAgent: Type.Optional(Type.String()),
717
+ preferredModel: Type.Optional(Type.String()),
718
+ preferredThinking: Type.Optional(ThinkingSchema),
719
+ includeReview: Type.Optional(Type.Boolean()),
720
+ checklist: Type.Optional(Type.Array(Type.Union([
721
+ Type.String(),
722
+ Type.Object({ text: Type.String(), done: Type.Optional(Type.Boolean()) }),
723
+ ]))),
724
+ checklistUpdates: Type.Optional(Type.Array(Type.Object({
725
+ text: Type.Optional(Type.String()),
726
+ index: Type.Optional(Type.Number()),
727
+ done: Type.Optional(Type.Boolean()),
728
+ }))),
729
+ tasks: Type.Optional(Type.Array(Type.Object({
730
+ id: Type.Optional(Type.String()),
731
+ title: Type.String(),
732
+ role: StringEnum(["general", "orchestrator", "architect", "design", "code", "review"] as const),
733
+ stage: Type.Optional(StringEnum(["genesis", "design", "build"] as const)),
734
+ workflow: Type.Optional(StringEnum(["vibe-genesis", "vibe-design", "vibe-build"] as const)),
735
+ parentTaskId: Type.Optional(Type.String()),
736
+ preferredAgent: Type.Optional(Type.String()),
737
+ preferredModel: Type.Optional(Type.String()),
738
+ preferredModelHint: Type.Optional(Type.String()),
739
+ preferredThinking: Type.Optional(ThinkingSchema),
740
+ fallbackModels: Type.Optional(Type.Array(Type.String())),
741
+ dispatchPolicy: Type.Optional(StringEnum(["direct", "subagent", "review-first"] as const)),
742
+ skills: Type.Optional(Type.Array(Type.String())),
743
+ checklist: Type.Optional(Type.Array(Type.Union([
744
+ Type.String(),
745
+ Type.Object({ text: Type.String(), done: Type.Optional(Type.Boolean()) }),
746
+ ]))),
747
+ objective: Type.Optional(Type.String()),
748
+ scope: Type.Optional(Type.Array(Type.String())),
749
+ definitionOfDone: Type.Optional(Type.Array(Type.String())),
750
+ expectedArtifacts: Type.Optional(Type.Array(Type.String())),
751
+ dependencies: Type.Optional(Type.Array(Type.String())),
752
+ reviewCheckpoint: Type.Optional(Type.String()),
753
+ instructions: Type.Optional(Type.Array(Type.String())),
754
+ conversationId: Type.Optional(Type.String()),
755
+ }))),
756
+ }),
757
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
758
+ if (params.action === "show_workflows") {
759
+ const workflows = listWorkflowDefinitions();
760
+ return {
761
+ content: [{ type: "text", text: workflows.map((workflow) => `${workflow.id}: ${workflow.playbook}`).join("\n\n") }],
762
+ details: { workflows },
763
+ };
764
+ }
765
+
766
+ if (params.action === "show_session") {
767
+ if (!params.sessionId) {
768
+ return { content: [{ type: "text", text: "sessionId is required for show_session" }], details: {}, isError: true };
769
+ }
770
+ const paths = getSessionPaths(ctx.cwd, params.sessionId);
771
+ const [masterPlan, stateJson] = await Promise.all([
772
+ readFile(paths.masterPlan, "utf8").catch(() => "Master plan not found."),
773
+ readFile(paths.stateFile, "utf8").catch(() => "{}"),
774
+ ]);
775
+ return {
776
+ content: [{ type: "text", text: `${masterPlan}\n\n---\n\nMachine state\n\n\
777
+ ${stateJson}` }],
778
+ details: { paths, state: normalizeSessionState({ sessionId: params.sessionId, title: "Takomi Session", ...(JSON.parse(stateJson) as Partial<OrchestratorSessionState>) }) },
779
+ };
780
+ }
781
+
782
+ if (params.action === "update_task") {
783
+ if (!params.sessionId || !params.taskId) {
784
+ return { content: [{ type: "text", text: "sessionId and taskId are required for update_task" }], details: {}, isError: true };
785
+ }
786
+ const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
787
+ const idx = sessionState.tasks.findIndex((task) => task.id === params.taskId);
788
+ if (idx === -1) {
789
+ return { content: [{ type: "text", text: `Task ${params.taskId} not found in session ${params.sessionId}` }], details: {}, isError: true };
790
+ }
791
+ const current = sessionState.tasks[idx];
792
+ const checklist = resolveChecklistState(current.checklist, params.checklist, params.checklistUpdates);
793
+ const nextTask = {
794
+ ...current,
795
+ status: (params.status ?? current.status) as OrchestratorTaskStatus,
796
+ notes: params.notes ?? current.notes,
797
+ checklist,
798
+ };
799
+ if (params.status === "completed") {
800
+ const completionGateError = getCompletionGateError(nextTask);
801
+ if (completionGateError) {
802
+ return {
803
+ content: [{ type: "text", text: completionGateError }],
804
+ details: {
805
+ sessionId: params.sessionId,
806
+ taskId: current.id,
807
+ incompleteChecklistItems: getIncompleteChecklistItems(nextTask.checklist),
808
+ checklist: nextTask.checklist,
809
+ },
810
+ isError: true,
811
+ };
812
+ }
813
+ }
814
+ sessionState.tasks[idx] = nextTask;
815
+ const nextState = buildSessionState(
816
+ sessionState.sessionId,
817
+ sessionState.title,
818
+ sessionState.tasks,
819
+ new Date(),
820
+ {
821
+ sessionIntent: sessionState.sessionIntent,
822
+ lifecycle: sessionState.lifecycle,
823
+ },
824
+ );
825
+ const paths = await syncTaskArtifacts(ctx.cwd, nextState);
826
+ await syncBoardTaskRunState(
827
+ ctx,
828
+ nextState.tasks[idx],
829
+ nextTask.status === "completed"
830
+ ? "Board task completed."
831
+ : nextTask.status === "blocked"
832
+ ? "Board task blocked."
833
+ : undefined,
834
+ );
835
+ return {
836
+ content: [{ type: "text", text: `Updated task ${params.taskId} in session ${params.sessionId}.\nStatus: ${nextState.tasks[idx].status}` }],
837
+ details: { sessionId: params.sessionId, task: nextState.tasks[idx], paths, lifecycle: nextState.lifecycle },
838
+ };
839
+ }
840
+
841
+ if (params.action === "dispatch_tasks") {
842
+ if (!state.subagentsEnabled) {
843
+ return {
844
+ content: [{ type: "text", text: "Takomi subagents are disabled. Use /takomi subagents on before dispatching board tasks." }],
845
+ details: { sessionId: params.sessionId, taskIds: params.taskIds },
846
+ isError: true,
847
+ };
848
+ }
849
+ if (!params.sessionId || !params.taskIds?.length) {
850
+ return { content: [{ type: "text", text: "sessionId and taskIds are required for dispatch_tasks" }], details: {}, isError: true };
851
+ }
852
+ const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
853
+ const selectedTasks = params.taskIds.map((id) => sessionState.tasks.find((task) => task.id === id));
854
+ if (selectedTasks.some((task) => !task)) {
855
+ return {
856
+ content: [{ type: "text", text: `Some taskIds were not found in session ${params.sessionId}.` }],
857
+ details: { requestedTaskIds: params.taskIds, availableTaskIds: sessionState.tasks.map((task) => task.id) },
858
+ isError: true,
859
+ };
860
+ }
861
+ const boardTasks = selectedTasks as OrchestratorTask[];
862
+ const mode = params.dispatchMode ?? (boardTasks.length === 1 ? "single" : "parallel");
863
+ const toolTasks = boardTasks.map((task) => ({
864
+ agent: params.preferredAgent ?? resolveTaskAgent(task),
865
+ task: task.notes || task.title,
866
+ workflow: task.workflow,
867
+ skills: task.skills,
868
+ model: params.preferredModel ?? task.preferredModel,
869
+ fallbackModels: task.fallbackModels,
870
+ thinking: params.preferredThinking ?? task.preferredThinking,
871
+ conversationId: task.conversationId ?? task.id,
872
+ checklist: task.checklist,
873
+ }));
874
+ if (params.previewOnly || ((state.launchMode ?? activeProfile.launchMode) === "manual" && !params.confirmLaunch)) {
875
+ return executeTakomiSubagentTool(pi, {
876
+ ...(mode === "chain" ? { chain: toolTasks } : mode === "parallel" ? { tasks: toolTasks } : toolTasks[0]),
877
+ previewOnly: true,
878
+ agentScope: params.agentScope ?? "both",
879
+ confirmProjectAgents: params.confirmProjectAgents,
880
+ }, _signal, _onUpdate, ctx);
881
+ }
882
+ for (const task of boardTasks) task.status = "in-progress";
883
+ let nextState = buildSessionState(sessionState.sessionId, sessionState.title, sessionState.tasks, new Date(), {
884
+ sessionIntent: sessionState.sessionIntent,
885
+ lifecycle: sessionState.lifecycle,
886
+ });
887
+ const paths = await syncTaskArtifacts(ctx.cwd, nextState);
888
+ const toolResult = await executeTakomiSubagentTool(pi, {
889
+ ...(mode === "chain" ? { chain: toolTasks } : mode === "parallel" ? { tasks: toolTasks } : toolTasks[0]),
890
+ confirmLaunch: true,
891
+ agentScope: params.agentScope ?? "both",
892
+ confirmProjectAgents: params.confirmProjectAgents,
893
+ }, _signal, _onUpdate, ctx);
894
+ const results = (toolResult.details as { results?: TakomiDispatchResult[] }).results ?? [];
895
+ for (let index = 0; index < boardTasks.length; index++) {
896
+ const result = results[index];
897
+ const task = boardTasks[index];
898
+ if (!result) continue;
899
+ task.conversationId = result.conversationId;
900
+ task.notes = appendTaskNote(task.notes, "Model preflight", result.preflight);
901
+ task.notes = appendTaskNote(task.notes, "Last dispatch output", result.output || result.stderr);
902
+ if (result.code !== 0) task.status = "blocked";
903
+ if (result.model) task.preferredModel = result.model;
904
+ if (result.thinking) task.preferredThinking = result.thinking;
905
+ }
906
+ nextState = buildSessionState(sessionState.sessionId, sessionState.title, sessionState.tasks, new Date(), {
907
+ sessionIntent: sessionState.sessionIntent,
908
+ lifecycle: sessionState.lifecycle,
909
+ });
910
+ await syncTaskArtifacts(ctx.cwd, nextState);
911
+ return {
912
+ content: toolResult.content,
913
+ details: { ...toolResult.details, sessionId: params.sessionId, paths, lifecycle: nextState.lifecycle, mode },
914
+ isError: results.some((result) => result.code !== 0) || undefined,
915
+ };
916
+ }
917
+
918
+ if (params.action === "redispatch_task" || params.action === "review_and_redispatch") {
919
+ if (!state.subagentsEnabled) {
920
+ return {
921
+ content: [{ type: "text", text: "Takomi subagents are disabled. Use /takomi subagents on before redispatching." }],
922
+ details: { sessionId: params.sessionId, taskId: params.taskId },
923
+ isError: true,
924
+ };
925
+ }
926
+ if (!params.sessionId || !params.taskId) {
927
+ return { content: [{ type: "text", text: "sessionId and taskId are required for redispatch_task" }], details: {}, isError: true };
928
+ }
929
+ const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
930
+ const task = sessionState.tasks.find((item) => item.id === params.taskId);
931
+ if (!task) {
932
+ return { content: [{ type: "text", text: `Task ${params.taskId} not found in session ${params.sessionId}` }], details: {}, isError: true };
933
+ }
934
+ const draftChecklist = resolveChecklistState(task.checklist, params.checklist, params.checklistUpdates);
935
+ const agentName = params.preferredAgent ?? resolveTaskAgent(task);
936
+ const draftModel = params.preferredModel ?? task.preferredModel;
937
+ const draftThinking = params.preferredThinking ?? task.preferredThinking;
938
+ const conversationId = task.conversationId ?? task.id;
939
+ const launchMode = state.launchMode ?? activeProfile.launchMode ?? "auto";
940
+ const plan = createTakomiDelegationPlan({
941
+ source: "runtime-board",
942
+ sessionId: params.sessionId,
943
+ launchMode,
944
+ profile: activeProfile,
945
+ tasks: [{
946
+ id: task.id,
947
+ title: task.title,
948
+ agent: agentName,
949
+ task: params.rerunInstructions ?? task.notes ?? task.title,
950
+ role: task.role,
951
+ stage: task.stage,
952
+ workflow: task.workflow,
953
+ model: draftModel,
954
+ fallbackModels: task.fallbackModels,
955
+ thinking: draftThinking,
956
+ conversationId,
957
+ checklist: draftChecklist,
958
+ dispatchPolicy: task.dispatchPolicy,
959
+ review: params.includeReview,
960
+ }],
961
+ });
962
+ if (params.previewOnly || (launchMode === "manual" && !params.confirmLaunch)) {
963
+ return {
964
+ content: [{ type: "text", text: renderTakomiDelegationPlan(plan) }],
965
+ details: { sessionId: params.sessionId, taskId: task.id, plan },
966
+ };
967
+ }
968
+
969
+ const agents: TakomiAgentConfig[] = discoverProjectAgents(ctx.cwd);
970
+ const config = agents.find((agent: TakomiAgentConfig) => agent.name === agentName);
971
+ if (!config) {
972
+ return { content: [{ type: "text", text: `Preferred agent '${agentName}' not found.` }], details: { availableAgents: agents.map((agent: TakomiAgentConfig) => agent.name) }, isError: true };
973
+ }
974
+
975
+ task.status = "in-progress";
976
+ task.checklist = draftChecklist;
977
+ task.preferredAgent = agentName;
978
+ task.preferredModel = draftModel;
979
+ task.preferredThinking = draftThinking;
980
+ if (params.action === "review_and_redispatch") {
981
+ task.notes = appendTaskNote(task.notes, "Review feedback", params.notes);
982
+ } else if (params.notes) {
983
+ task.notes = params.notes;
984
+ }
985
+ let nextState = buildSessionState(
986
+ sessionState.sessionId,
987
+ sessionState.title,
988
+ sessionState.tasks,
989
+ new Date(),
990
+ {
991
+ sessionIntent: sessionState.sessionIntent,
992
+ lifecycle: sessionState.lifecycle,
993
+ },
994
+ );
995
+ const paths = await syncTaskArtifacts(ctx.cwd, nextState);
996
+ task.conversationId = conversationId;
997
+ const runKey = conversationId;
998
+ const parentRunKey = task.parentTaskId
999
+ ? (() => {
1000
+ const parentTask = sessionState.tasks.find((item) => item.id === task.parentTaskId);
1001
+ if (parentTask) return parentTask.conversationId ?? parentTask.id;
1002
+ return subagentController.getKnownParentRunKey(task.parentTaskId);
1003
+ })()
1004
+ : undefined;
1005
+
1006
+ const result = await dispatchTakomiSubagent(ctx, {
1007
+ agent: config,
1008
+ task: task.notes || task.title,
1009
+ rootCwd: ctx.cwd,
1010
+ workflow: task.workflow,
1011
+ skills: task.skills,
1012
+ model: task.preferredModel,
1013
+ fallbackModels: task.fallbackModels,
1014
+ thinking: task.preferredThinking,
1015
+ conversationId,
1016
+ checklist: task.checklist,
1017
+ stage: task.stage,
1018
+ taskLabel: `${task.id} - ${task.title}`,
1019
+ parentTaskId: task.parentTaskId,
1020
+ parentRunKey,
1021
+ boardTaskStatus: task.status,
1022
+ source: "runtime-board",
1023
+ rerunInstructions: params.rerunInstructions,
1024
+ }, _signal, {
1025
+ emit: (event) => {
1026
+ void applySubagentRuntimeEvent(event, ctx);
1027
+ },
1028
+ });
1029
+
1030
+ task.notes = appendTaskNote(task.notes, "Model preflight", result.preflight);
1031
+ if (result.model) task.preferredModel = result.model;
1032
+ if (result.warning) {
1033
+ task.notes = appendTaskNote(task.notes, "Model fallback", result.warning);
1034
+ if (ctx.hasUI) ctx.ui.notify(result.warning, "warning");
1035
+ }
1036
+ if (result.thinking) task.preferredThinking = result.thinking;
1037
+
1038
+ if (result.code !== 0) {
1039
+ task.status = "blocked";
1040
+ task.notes = appendTaskNote(task.notes, "Redispatch failure", result.stderr || result.output);
1041
+ nextState = buildSessionState(
1042
+ sessionState.sessionId,
1043
+ sessionState.title,
1044
+ sessionState.tasks,
1045
+ new Date(),
1046
+ {
1047
+ sessionIntent: sessionState.sessionIntent,
1048
+ lifecycle: sessionState.lifecycle,
1049
+ },
1050
+ );
1051
+ await syncTaskArtifacts(ctx.cwd, nextState);
1052
+ return {
1053
+ content: [{ type: "text", text: `Redispatch failed for task ${task.id}.\n\n${result.stderr || result.output || "No output"}` }],
1054
+ details: { sessionId: params.sessionId, task, paths, result },
1055
+ isError: true,
1056
+ };
1057
+ }
1058
+
1059
+ task.notes = appendTaskNote(task.notes, "Last redispatch output", result.output);
1060
+ nextState = buildSessionState(
1061
+ sessionState.sessionId,
1062
+ sessionState.title,
1063
+ sessionState.tasks,
1064
+ new Date(),
1065
+ {
1066
+ sessionIntent: sessionState.sessionIntent,
1067
+ lifecycle: sessionState.lifecycle,
1068
+ },
1069
+ );
1070
+ await syncTaskArtifacts(ctx.cwd, nextState);
1071
+ return {
1072
+ content: [{ type: "text", text: `${result.preflight}\n\n${result.output || `Redispatched task ${task.id} to ${agentName}.`}` }],
1073
+ details: { sessionId: params.sessionId, task, paths, lifecycle: nextState.lifecycle, agent: agentName, conversationId: task.conversationId, action: params.action, result },
1074
+ };
1075
+ }
1076
+
1077
+ if (params.action === "expand_stage") {
1078
+ if (!params.sessionId || !params.stage || !params.tasks?.length) {
1079
+ return { content: [{ type: "text", text: "sessionId, stage, and at least one task are required for expand_stage" }], details: {}, isError: true };
1080
+ }
1081
+
1082
+ const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
1083
+ const tasks = await materializeTasksFromInput(ctx, sessionState.tasks, params.tasks as IncomingTask[], params.stage);
1084
+ let nextState = buildSessionState(
1085
+ sessionState.sessionId,
1086
+ sessionState.title,
1087
+ tasks,
1088
+ new Date(),
1089
+ {
1090
+ sessionIntent: sessionState.sessionIntent,
1091
+ lifecycle: sessionState.lifecycle,
1092
+ },
1093
+ );
1094
+ nextState = markStageExpanded(nextState, params.stage, params.notes);
1095
+ const paths = await writeOrchestratorSession(ctx.cwd, nextState);
1096
+ state.activeSessionId = nextState.sessionId;
1097
+ persistState();
1098
+ syncContextPanelState();
1099
+
1100
+ return {
1101
+ content: [{ type: "text", text: `Expanded ${params.stage} stage in session ${nextState.sessionId}.\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks)}` }],
1102
+ details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode },
1103
+ };
1104
+ }
1105
+
1106
+ const sessionId = params.sessionId || createSessionId();
1107
+ const title = params.title || "Takomi Session";
1108
+ const baseState = params.tasks?.length
1109
+ ? buildSessionState(sessionId, title, [], new Date())
1110
+ : createLifecycleStarterSession(title, { sessionId });
1111
+ const tasks = params.tasks?.length
1112
+ ? await materializeTasksFromInput(ctx, baseState.tasks, params.tasks as IncomingTask[], params.stage)
1113
+ : await applyProfileDefaultsToTasks(ctx, baseState.tasks);
1114
+ const nextState = buildSessionState(
1115
+ baseState.sessionId,
1116
+ baseState.title,
1117
+ tasks,
1118
+ new Date(),
1119
+ {
1120
+ sessionIntent: baseState.sessionIntent,
1121
+ lifecycle: baseState.lifecycle,
1122
+ },
1123
+ );
1124
+ const paths = await writeOrchestratorSession(ctx.cwd, nextState);
1125
+ state.activeSessionId = nextState.sessionId;
1126
+ state.role = "orchestrator";
1127
+ state.stage = nextState.lifecycle.genesis.status === "completed" ? "build" : "genesis";
1128
+ state.workflow = state.stage === "genesis" ? "vibe-genesis" : "vibe-build";
1129
+ persistState();
1130
+ syncContextPanelState();
1131
+
1132
+ return {
1133
+ content: [{ type: "text", text: `Created Takomi orchestrator session ${nextState.sessionId} in hybrid mode\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks) || "No tasks provided."}` }],
1134
+ details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode },
1135
+ };
1136
+ },
1137
+ });
1138
+
1139
+ pi.on("input", async (event) => {
1140
+ if (event.source === "extension") return { action: "continue" };
1141
+
1142
+ const text = event.text.trim();
1143
+ const lowered = text.toLowerCase();
1144
+
1145
+ const routingUpdateMatch = text.match(/^update\s+(?:takomi\s+|our\s+)?(?:model\s+)?routing\s+(?:logic|policy|philosophy)\s*:?\s*([\s\S]*)$/i)
1146
+ ?? text.match(/^set\s+(?:takomi\s+|our\s+)?(?:model\s+)?routing\s+(?:logic|policy|philosophy)\s*:?\s*([\s\S]*)$/i);
1147
+ if (routingUpdateMatch) {
1148
+ state.enabled = true;
1149
+ try {
1150
+ const result = await installTakomiRoutingPolicy(runtimeCtx?.cwd ?? process.cwd(), text);
1151
+ const detected = result.detectedDefaults.length ? `\n\nDetected defaults:\n- ${result.detectedDefaults.join("\n- ")}` : "\n\nNo model names were auto-detected; saved policy only.";
1152
+ 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.` };
1153
+ } catch (error) {
1154
+ return { action: "transform", text: `Takomi routing policy update failed: ${error instanceof Error ? error.message : String(error)}` };
1155
+ }
1156
+ }
1157
+
1158
+ if (lowered === "use takomi") {
1159
+ state.enabled = true;
1160
+ return { action: "transform", text: "Use the Takomi runtime, identify the correct lifecycle stage, and proceed accordingly." };
1161
+ }
1162
+
1163
+ if (lowered.startsWith("use takomi ")) {
1164
+ state.enabled = true;
1165
+ const route = decideRoute(text.slice("use takomi ".length));
1166
+ if (route.stage) setStageAndWorkflow(state, route.stage, { preserveRole: state.role === "orchestrator" && route.stage === "genesis" });
1167
+ else if (route.role !== "general") state.role = route.role;
1168
+ return { action: "transform", text: `Use the Takomi runtime for this request: ${text.slice("use takomi ".length)}` };
1169
+ }
1170
+
1171
+ if (/\bvibe genesis\b/i.test(text)) {
1172
+ setStageAndWorkflow(state, "genesis", { preserveRole: state.role === "orchestrator" });
1173
+ return { action: "transform", text };
1174
+ }
1175
+ if (/\bvibe design\b/i.test(text)) {
1176
+ setStageAndWorkflow(state, "design");
1177
+ return { action: "transform", text };
1178
+ }
1179
+ if (/\bvibe build\b/i.test(text)) {
1180
+ setStageAndWorkflow(state, "build");
1181
+ return { action: "transform", text };
1182
+ }
1183
+
1184
+ return { action: "continue" };
1185
+ });
1186
+
1187
+ pi.on("before_agent_start", async (event) => {
1188
+ if (!state.enabled) return;
1189
+
1190
+ let effectiveState = cloneState(state);
1191
+ const runtimeCwd = typeof (event as { cwd?: string }).cwd === "string" ? (event as { cwd?: string }).cwd as string : process.cwd();
1192
+ const genesisExists = await hasGenesisArtifacts(runtimeCwd);
1193
+ const route = decideRoute(event.prompt);
1194
+ if (state.autoOrch && shouldAutoRoute(event.prompt)) {
1195
+ effectiveState.role = "orchestrator";
1196
+ effectiveState.stage = genesisExists ? "build" : "genesis";
1197
+ effectiveState.workflow = genesisExists ? "vibe-build" : "vibe-genesis";
1198
+ }
1199
+
1200
+ const shouldHonorRoute = route.stage || route.role !== "general" || route.sessionRecommendation !== "none";
1201
+ if (shouldHonorRoute && route.stage) {
1202
+ effectiveState.stage = route.stage;
1203
+ effectiveState.workflow = route.workflow;
1204
+ effectiveState.role = effectiveState.role === "orchestrator" && route.stage === "genesis" ? "orchestrator" : route.role;
1205
+ } else if (shouldHonorRoute && route.role !== "general") {
1206
+ effectiveState.role = route.role;
1207
+ }
1208
+
1209
+ let routingNote = route.reason;
1210
+ const explicitLifecycleWaiver = /skip genesis|waive genesis|genesis complete|already have (a )?(prd|requirements)|design complete|jump straight to build/i.test(event.prompt);
1211
+ const orchestrationActive = effectiveState.role === "orchestrator" || route.executionMode === "orchestrate";
1212
+ if (!genesisExists && orchestrationActive && !explicitLifecycleWaiver) {
1213
+ effectiveState.stage = "genesis";
1214
+ effectiveState.workflow = "vibe-genesis";
1215
+ routingNote = "Blank project detected; orchestrator remains in control and must honor Genesis → Design → Build.";
1216
+ }
1217
+
1218
+ const routingPolicy = await loadTakomiRoutingPolicy(runtimeCwd);
1219
+ const modelPreflightContext = (() => {
1220
+ try {
1221
+ const available = typeof (runtimeCtx as { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } } | undefined)?.modelRegistry?.getAvailable === "function"
1222
+ ? (runtimeCtx as { modelRegistry: { getAvailable: () => Array<{ provider?: string; id?: string; name?: string }> } }).modelRegistry.getAvailable()
1223
+ : [];
1224
+ if (!available.length) return "";
1225
+ return `Available model context from Pi registry: ${available.map((m) => `${m.provider ? `${m.provider}/` : ""}${m.id ?? m.name ?? "unknown"}`).slice(0, 80).join(", ")}`;
1226
+ } catch {
1227
+ return "";
1228
+ }
1229
+ })();
1230
+
1231
+ const parts = [
1232
+ "Takomi runtime is active for this turn.",
1233
+ rolePrompt(effectiveState.role),
1234
+ effectiveState.planMode ? planPrompt() : "",
1235
+ getInjectedPlaybook(effectiveState),
1236
+ `Routing note: ${routingNote}`,
1237
+ routingPolicy ? `Project Takomi model routing policy is active. Apply it when choosing parent/subagent models and escalation levels:\n\n${routingPolicy}` : "No project Takomi model routing policy file was found. Users can install one with `/takomi routing <policy>` or by saying `Update Takomi routing logic: \"\"\"...\"\"\"`.",
1238
+ modelPreflightContext,
1239
+ `Execution mode: ${route.executionMode}. Session recommendation: ${route.sessionRecommendation}.`,
1240
+ `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.`,
1241
+ !effectiveState.subagentsEnabled ? "Takomi subagents are disabled for this session. Do not call takomi_subagent, subagent, or redispatch board tasks until the user enables subagents." : "",
1242
+ !genesisExists ? "Project foundation is missing or incomplete. Do not skip Genesis unless the user explicitly waives it." : "",
1243
+ "Takomi is the default orchestration mindset here. Do not wait for the literal phrase 'use Takomi' before applying lifecycle judgment.",
1244
+ "Task fan-out is flexible. Do not force exactly three tasks; decompose Genesis, Design, and Build work to fit the actual scope.",
1245
+ "A new orchestration session should usually begin with one Genesis foundation task that creates or updates the required markdown artifacts, then expand later stages only when the scope justifies it.",
1246
+ "If a follow-up request is small, one-shot it. If it is multi-part or large, create or expand an orchestration session instead of pretending it is a single task.",
1247
+ "Before any Takomi subagent dispatch or model override, use the injected Pi model-registry context and project routing policy. Prefer provider-qualified model IDs. Do not run `pi --list-models` unless the registry context is missing or the user asks for a visible diagnostic.",
1248
+ "When useful, state the current Takomi stage and the recommended next stage.",
1249
+ effectiveState.stage === "build"
1250
+ ? "For build orchestration, it is valid to dispatch tasks to specialist subagents, review them, and send fixes back to the same agent by reusing its conversation id."
1251
+ : "",
1252
+ ].filter(Boolean);
1253
+
1254
+ return {
1255
+ systemPrompt: `${event.systemPrompt}\n\n${parts.join("\n\n")}`,
1256
+ };
1257
+ });
1258
+
1259
+ pi.on("session_start", async (_event, ctx) => {
1260
+ runtimeCtx = ctx;
1261
+ activeProfile = await loadTakomiProfile(ctx.cwd);
1262
+ activeSubagentLabel = undefined;
1263
+ subagentController.reset(ctx);
1264
+ contextPanel.resetSession();
1265
+ const entries = ctx.sessionManager.getEntries();
1266
+ for (let i = entries.length - 1; i >= 0; i--) {
1267
+ const entry = entries[i] as { type: string; customType?: string; data?: TakomiState };
1268
+ if (entry.type === "custom" && entry.customType === STATE_ENTRY && entry.data) {
1269
+ state = { ...DEFAULT_STATE, ...entry.data };
1270
+ break;
1271
+ }
1272
+ }
1273
+ if (!entries.some((entry) => {
1274
+ const item = entry as { type: string; customType?: string };
1275
+ return item.type === "custom" && item.customType === STATE_ENTRY;
1276
+ })) {
1277
+ state.autoOrch = activeProfile.autoOrchestrate;
1278
+ state.launchMode = activeProfile.launchMode ?? (activeProfile.autoOrchestrate ? "auto" : "manual");
1279
+ } else {
1280
+ state.launchMode = state.launchMode ?? activeProfile.launchMode ?? "auto";
1281
+ }
1282
+
1283
+ syncContextPanelState();
1284
+ await refreshUi(ctx, state);
1285
+ contextPanel.show(ctx);
1286
+ flushPendingSubagentEvents();
1287
+ });
1288
+ }