takomi 2.1.2 → 2.1.3

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 (49) 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 +492 -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 +83 -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 +215 -215
  35. package/.pi/extensions/takomi-subagents/index.ts +75 -75
  36. package/.pi/extensions/takomi-subagents/live-updates.ts +83 -83
  37. package/.pi/extensions/takomi-subagents/native-render.ts +174 -174
  38. package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -209
  39. package/.pi/themes/takomi-noir.json +81 -81
  40. package/package.json +59 -59
  41. package/src/doctor.js +87 -84
  42. package/src/pi-harness.js +355 -351
  43. package/src/pi-installer.js +193 -171
  44. package/src/pi-takomi-core/index.ts +4 -4
  45. package/src/pi-takomi-core/orchestration.ts +402 -402
  46. package/src/pi-takomi-core/routing.ts +93 -93
  47. package/src/pi-takomi-core/types.ts +173 -173
  48. package/src/pi-takomi-core/workflows.ts +299 -299
  49. package/src/skills-installer.js +101 -101
@@ -1,402 +1,402 @@
1
- import path from "node:path";
2
- import type {
3
- LifecycleStageState,
4
- OrchestratorSessionState,
5
- OrchestratorTask,
6
- OrchestratorTaskStatus,
7
- SessionIntent,
8
- TakomiRole,
9
- TaskChecklistItem,
10
- VibeLifecycleStage,
11
- } from "./types";
12
-
13
- export function createSessionId(now = new Date()): string {
14
- const yyyy = now.getFullYear();
15
- const mm = String(now.getMonth() + 1).padStart(2, "0");
16
- const dd = String(now.getDate()).padStart(2, "0");
17
- const hh = String(now.getHours()).padStart(2, "0");
18
- const mi = String(now.getMinutes()).padStart(2, "0");
19
- const ss = String(now.getSeconds()).padStart(2, "0");
20
- return `orch-${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
21
- }
22
-
23
- export function slugifyTaskTitle(title: string): string {
24
- return title.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
25
- }
26
-
27
- export function getSessionPaths(cwd: string, sessionId: string) {
28
- const docsRoot = path.join(cwd, "docs", "tasks", "orchestrator-sessions", sessionId);
29
- const machineRoot = path.join(cwd, ".pi", "takomi", "orchestrator");
30
- return {
31
- root: docsRoot,
32
- pending: path.join(docsRoot, "pending"),
33
- inProgress: path.join(docsRoot, "in-progress"),
34
- completed: path.join(docsRoot, "completed"),
35
- blocked: path.join(docsRoot, "blocked"),
36
- masterPlan: path.join(docsRoot, "master_plan.md"),
37
- summary: path.join(docsRoot, "Orchestrator_Summary.md"),
38
- stateDir: machineRoot,
39
- stateFile: path.join(machineRoot, `${sessionId}.json`),
40
- };
41
- }
42
-
43
- export function createConversationId(agent: string, taskId: string): string {
44
- return `${agent}-${taskId}`;
45
- }
46
-
47
- export function workflowToStage(workflow?: OrchestratorTask["workflow"]): VibeLifecycleStage | undefined {
48
- switch (workflow) {
49
- case "vibe-genesis":
50
- return "genesis";
51
- case "vibe-design":
52
- return "design";
53
- case "vibe-build":
54
- return "build";
55
- default:
56
- return undefined;
57
- }
58
- }
59
-
60
- function defaultStageForRole(role: TakomiRole): VibeLifecycleStage | undefined {
61
- switch (role) {
62
- case "architect":
63
- return "genesis";
64
- case "design":
65
- return "design";
66
- case "code":
67
- case "review":
68
- case "orchestrator":
69
- return "build";
70
- default:
71
- return undefined;
72
- }
73
- }
74
-
75
- function emptyStageState(): LifecycleStageState {
76
- return {
77
- status: "pending",
78
- taskIds: [],
79
- canExpand: true,
80
- };
81
- }
82
-
83
- export function createEmptyLifecycle(): Record<VibeLifecycleStage, LifecycleStageState> {
84
- return {
85
- genesis: emptyStageState(),
86
- design: emptyStageState(),
87
- build: emptyStageState(),
88
- };
89
- }
90
-
91
- function stageStatusFromTasks(tasks: OrchestratorTask[]): OrchestratorTaskStatus {
92
- if (!tasks.length) return "pending";
93
- if (tasks.every((task) => task.status === "completed")) return "completed";
94
- if (tasks.some((task) => task.status === "in-progress")) return "in-progress";
95
- if (tasks.some((task) => task.status === "blocked") && !tasks.some((task) => task.status === "completed")) return "blocked";
96
- if (tasks.some((task) => task.status === "completed")) return "in-progress";
97
- return "pending";
98
- }
99
-
100
- export function deriveLifecycleFromTasks(
101
- tasks: OrchestratorTask[],
102
- previous?: Partial<Record<VibeLifecycleStage, LifecycleStageState>>,
103
- ): Record<VibeLifecycleStage, LifecycleStageState> {
104
- const lifecycle = createEmptyLifecycle();
105
-
106
- for (const stage of Object.keys(lifecycle) as VibeLifecycleStage[]) {
107
- const stageTasks = tasks.filter((task) => task.stage === stage || workflowToStage(task.workflow) === stage);
108
- const prev = previous?.[stage];
109
- lifecycle[stage] = {
110
- status: stageStatusFromTasks(stageTasks),
111
- taskIds: stageTasks.map((task) => task.id),
112
- canExpand: prev?.canExpand ?? true,
113
- expandedAt: prev?.expandedAt,
114
- notes: prev?.notes,
115
- };
116
- }
117
-
118
- return lifecycle;
119
- }
120
-
121
- export function normalizeChecklist(checklist?: Array<string | TaskChecklistItem>): TaskChecklistItem[] | undefined {
122
- if (!checklist?.length) return undefined;
123
- return checklist.map((item) => typeof item === "string" ? { text: item, done: false } : { text: item.text, done: item.done ?? false });
124
- }
125
-
126
- export function createTask(id: string, title: string, role: TakomiRole, extras?: Partial<OrchestratorTask>): OrchestratorTask {
127
- const preferredAgent = extras?.preferredAgent ?? (role === "design" ? "designer" : role === "architect" ? "architect" : role === "review" ? "reviewer" : role === "code" ? "coder" : "orchestrator");
128
- const stage = extras?.stage ?? workflowToStage(extras?.workflow) ?? defaultStageForRole(role);
129
- return {
130
- id,
131
- title,
132
- role,
133
- status: "pending",
134
- ...extras,
135
- stage,
136
- preferredAgent,
137
- conversationId: extras?.conversationId ?? createConversationId(preferredAgent, id),
138
- preferredModel: extras?.preferredModel,
139
- preferredModelHint: extras?.preferredModelHint,
140
- preferredThinking: extras?.preferredThinking,
141
- fallbackModels: extras?.fallbackModels,
142
- dispatchPolicy: extras?.dispatchPolicy,
143
- skills: extras?.skills,
144
- checklist: normalizeChecklist(extras?.checklist),
145
- };
146
- }
147
-
148
- export function getNextTaskId(tasks: OrchestratorTask[]): string {
149
- const max = tasks.reduce((current, task) => {
150
- const parsed = Number.parseInt(task.id, 10);
151
- return Number.isFinite(parsed) ? Math.max(current, parsed) : current;
152
- }, 0);
153
- return String(max + 1).padStart(2, "0");
154
- }
155
-
156
- export function moveTaskStatus(tasks: OrchestratorTask[], id: string, status: OrchestratorTaskStatus): OrchestratorTask[] {
157
- return tasks.map((task) => (task.id === id ? { ...task, status } : task));
158
- }
159
-
160
- function renderChecklist(checklist?: TaskChecklistItem[]): string[] {
161
- if (!checklist?.length) return ["- No checklist yet."];
162
- return checklist.map((item) => `- [${item.done ? "x" : " "}] ${item.text}`);
163
- }
164
-
165
- function renderBullets(items?: string[], empty = "- None specified."): string[] {
166
- if (!items?.length) return [empty];
167
- return items.map((item) => `- ${item}`);
168
- }
169
-
170
- function renderLifecycleSummary(state: OrchestratorSessionState): string[] {
171
- return (Object.keys(state.lifecycle) as VibeLifecycleStage[]).flatMap((stage) => {
172
- const entry = state.lifecycle[stage];
173
- const taskSummary = entry.taskIds.length ? entry.taskIds.join(", ") : "none yet";
174
- return [
175
- `### ${stage[0].toUpperCase()}${stage.slice(1)}`,
176
- `- Status: ${entry.status}`,
177
- `- Tasks: ${taskSummary}`,
178
- `- Expandable: ${entry.canExpand === false ? "no" : "yes"}`,
179
- entry.expandedAt ? `- Expanded At: ${entry.expandedAt}` : "",
180
- entry.notes ? `- Notes: ${entry.notes}` : "",
181
- "",
182
- ].filter(Boolean);
183
- });
184
- }
185
-
186
- export function renderMasterPlan(sessionOrId: OrchestratorSessionState | string, title?: string, tasks?: OrchestratorTask[]): string {
187
- const state = typeof sessionOrId === "string"
188
- ? buildSessionState(sessionOrId, title ?? "Takomi Session", tasks ?? [])
189
- : normalizeSessionState(sessionOrId);
190
-
191
- const rows = state.tasks
192
- .map((task) => `| ${task.id} | ${task.stage ?? "-"} | ${task.title} | ${task.status} | ${task.role} | ${task.preferredAgent ?? "-"} | ${task.workflow ?? "-"} | ${task.preferredModel ?? task.preferredModelHint ?? "-"} | ${task.preferredThinking ?? "-"} | ${task.dispatchPolicy ?? "-"} | ${task.skills?.join(", ") ?? "-"} |`)
193
- .join("\n");
194
-
195
- return [
196
- `# Master Plan: ${state.title}`,
197
- "",
198
- `**Session ID:** ${state.sessionId}`,
199
- `**Runtime Mode:** ${state.mode}`,
200
- `**Session Intent:** ${state.sessionIntent ?? "full-project"}`,
201
- "",
202
- "## Lifecycle",
203
- "",
204
- ...renderLifecycleSummary(state),
205
- "## Tasks",
206
- "",
207
- "| ID | Stage | Title | Status | Role | Preferred Agent | Workflow | Model | Thinking | Dispatch | Skills |",
208
- "|---|---|---|---|---|---|---|---|---|---|---|",
209
- rows || "| - | - | No tasks yet | - | - | - | - | - | - | - | - |",
210
- "",
211
- "## Notes",
212
- "",
213
- "- Human-readable task docs live in this session folder.",
214
- "- Machine state lives in `.pi/takomi/orchestrator/<sessionId>.json`.",
215
- "- Sending a task back to the same agent should reuse its conversationId when continuity is helpful.",
216
- "- Sessions follow the Genesis -> Design -> Build lifecycle, but each stage may stay compact or expand into more tasks.",
217
- ].join("\n");
218
- }
219
-
220
- export function renderTaskFile(task: OrchestratorTask, context?: string): string {
221
- return [
222
- `# Task: ${task.title}`,
223
- "",
224
- `**Task ID:** ${task.id}`,
225
- `**Stage:** ${task.stage ?? "-"}`,
226
- `**Status:** ${task.status}`,
227
- `**Role:** ${task.role}`,
228
- task.parentTaskId ? `**Parent Task:** ${task.parentTaskId}` : "",
229
- `**Preferred Agent:** ${task.preferredAgent ?? "-"}`,
230
- `**Conversation ID:** ${task.conversationId ?? "-"}`,
231
- `**Workflow:** ${task.workflow ?? "-"}`,
232
- task.preferredModel ? `**Model Override:** ${task.preferredModel}` : "",
233
- task.preferredModelHint ? `**Model Hint:** ${task.preferredModelHint}` : "",
234
- task.fallbackModels?.length ? `**Fallback Models:** ${task.fallbackModels.join(", ")}` : "",
235
- task.preferredThinking ? `**Thinking Level:** ${task.preferredThinking}` : "",
236
- task.dispatchPolicy ? `**Dispatch Policy:** ${task.dispatchPolicy}` : "",
237
- task.skills?.length ? `**Required Skills:** ${task.skills.join(", ")}` : "",
238
- "",
239
- "## Context",
240
- "",
241
- context ?? "Add task-specific context here.",
242
- "",
243
- "## Objective",
244
- "",
245
- task.objective ?? task.title,
246
- "",
247
- "## Scope",
248
- "",
249
- ...renderBullets(task.scope),
250
- "",
251
- "## Checklist",
252
- "",
253
- ...renderChecklist(task.checklist),
254
- "",
255
- "## Definition of Done",
256
- "",
257
- ...renderBullets(task.definitionOfDone),
258
- "",
259
- "## Expected Artifacts",
260
- "",
261
- ...renderBullets(task.expectedArtifacts),
262
- "",
263
- "## Dependencies",
264
- "",
265
- ...renderBullets(task.dependencies),
266
- "",
267
- "## Review Checkpoint",
268
- "",
269
- task.reviewCheckpoint ?? "Review before implementation handoff or final completion.",
270
- "",
271
- "## Instructions",
272
- "",
273
- ...renderBullets(task.instructions ?? [
274
- "complete the task within scope",
275
- "use the listed workflow and skills when they are provided",
276
- "report blockers clearly",
277
- "if review sends this back, continue using the same conversation id when possible",
278
- "summarize what changed and what remains",
279
- ]),
280
- task.notes ? "" : "",
281
- task.notes ? "## Notes" : "",
282
- task.notes ? "" : "",
283
- task.notes ?? "",
284
- ].filter(Boolean).join("\n");
285
- }
286
-
287
- export function buildSessionState(
288
- sessionId: string,
289
- title: string,
290
- tasks: OrchestratorTask[],
291
- now = new Date(),
292
- extras?: Partial<Pick<OrchestratorSessionState, "sessionIntent" | "lifecycle">>,
293
- ): OrchestratorSessionState {
294
- const stamp = now.toISOString();
295
- const normalizedTasks = tasks.map((task) => ({ ...task, stage: task.stage ?? workflowToStage(task.workflow) ?? defaultStageForRole(task.role) }));
296
- return {
297
- sessionId,
298
- title,
299
- createdAt: stamp,
300
- updatedAt: stamp,
301
- mode: "hybrid",
302
- lifecycle: deriveLifecycleFromTasks(normalizedTasks, extras?.lifecycle),
303
- sessionIntent: extras?.sessionIntent ?? "full-project",
304
- tasks: normalizedTasks,
305
- };
306
- }
307
-
308
- export function normalizeSessionState(
309
- session: Partial<OrchestratorSessionState> & Pick<OrchestratorSessionState, "sessionId" | "title">,
310
- ): OrchestratorSessionState {
311
- const tasks = (session.tasks ?? []).map((task) => ({ ...task, stage: task.stage ?? workflowToStage(task.workflow) ?? defaultStageForRole(task.role) }));
312
- const normalized = buildSessionState(
313
- session.sessionId,
314
- session.title,
315
- tasks,
316
- session.updatedAt ? new Date(session.updatedAt) : new Date(),
317
- {
318
- sessionIntent: session.sessionIntent ?? "full-project",
319
- lifecycle: session.lifecycle,
320
- },
321
- );
322
-
323
- return {
324
- ...normalized,
325
- createdAt: session.createdAt ?? normalized.createdAt,
326
- updatedAt: session.updatedAt ?? normalized.updatedAt,
327
- mode: "hybrid",
328
- };
329
- }
330
-
331
- export function markStageExpanded(
332
- state: OrchestratorSessionState,
333
- stage: VibeLifecycleStage,
334
- notes?: string,
335
- now = new Date(),
336
- ): OrchestratorSessionState {
337
- const lifecycle = {
338
- ...state.lifecycle,
339
- [stage]: {
340
- ...state.lifecycle[stage],
341
- expandedAt: now.toISOString(),
342
- notes: notes ?? state.lifecycle[stage].notes,
343
- },
344
- };
345
- return normalizeSessionState({
346
- ...state,
347
- updatedAt: now.toISOString(),
348
- lifecycle,
349
- });
350
- }
351
-
352
- export function createLifecycleStarterSession(
353
- title: string,
354
- options?: { sessionId?: string; now?: Date; sessionIntent?: SessionIntent },
355
- ): OrchestratorSessionState {
356
- const sessionId = options?.sessionId ?? createSessionId(options?.now);
357
- const tasks = [
358
- createTask("01", "Genesis foundation", "orchestrator", {
359
- stage: "genesis",
360
- workflow: "vibe-genesis",
361
- preferredAgent: "orchestrator",
362
- objective: "Establish the project foundation, produce the required planning docs, and decide what should split next.",
363
- scope: [
364
- "Clarify scope and mission",
365
- "Create or update the core markdown artifacts",
366
- "Lock acceptance criteria and boundaries",
367
- "Recommend whether Design and Build should stay compact or expand",
368
- ],
369
- checklist: [
370
- { text: "Create or update requirements docs" },
371
- { text: "Capture acceptance criteria" },
372
- { text: "Define boundaries and non-goals" },
373
- { text: "Recommend next-stage task breakdown" },
374
- ],
375
- definitionOfDone: [
376
- "Required planning markdown files exist or are updated",
377
- "Minimum usable state is explicit",
378
- "Genesis recommends the correct next Design and Build structure",
379
- ],
380
- expectedArtifacts: [
381
- "Requirements and feature docs",
382
- "Genesis brief",
383
- "Recommended task breakdown for later stages",
384
- ],
385
- reviewCheckpoint: "User or orchestrator approves the foundation before expanding later stages.",
386
- instructions: [
387
- "treat this as the root task for the whole Genesis -> Design -> Build lifecycle",
388
- "create the required markdown artifacts before implementation begins",
389
- "split later-stage work only when the scope justifies it",
390
- "leave a clear recommendation for how Design and Build should fan out",
391
- ],
392
- }),
393
- ];
394
-
395
- return buildSessionState(sessionId, title, tasks, options?.now, {
396
- sessionIntent: options?.sessionIntent ?? "full-project",
397
- });
398
- }
399
-
400
- export function serializeSessionState(state: OrchestratorSessionState): string {
401
- return `${JSON.stringify(normalizeSessionState(state), null, 2)}\n`;
402
- }
1
+ import path from "node:path";
2
+ import type {
3
+ LifecycleStageState,
4
+ OrchestratorSessionState,
5
+ OrchestratorTask,
6
+ OrchestratorTaskStatus,
7
+ SessionIntent,
8
+ TakomiRole,
9
+ TaskChecklistItem,
10
+ VibeLifecycleStage,
11
+ } from "./types";
12
+
13
+ export function createSessionId(now = new Date()): string {
14
+ const yyyy = now.getFullYear();
15
+ const mm = String(now.getMonth() + 1).padStart(2, "0");
16
+ const dd = String(now.getDate()).padStart(2, "0");
17
+ const hh = String(now.getHours()).padStart(2, "0");
18
+ const mi = String(now.getMinutes()).padStart(2, "0");
19
+ const ss = String(now.getSeconds()).padStart(2, "0");
20
+ return `orch-${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
21
+ }
22
+
23
+ export function slugifyTaskTitle(title: string): string {
24
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
25
+ }
26
+
27
+ export function getSessionPaths(cwd: string, sessionId: string) {
28
+ const docsRoot = path.join(cwd, "docs", "tasks", "orchestrator-sessions", sessionId);
29
+ const machineRoot = path.join(cwd, ".pi", "takomi", "orchestrator");
30
+ return {
31
+ root: docsRoot,
32
+ pending: path.join(docsRoot, "pending"),
33
+ inProgress: path.join(docsRoot, "in-progress"),
34
+ completed: path.join(docsRoot, "completed"),
35
+ blocked: path.join(docsRoot, "blocked"),
36
+ masterPlan: path.join(docsRoot, "master_plan.md"),
37
+ summary: path.join(docsRoot, "Orchestrator_Summary.md"),
38
+ stateDir: machineRoot,
39
+ stateFile: path.join(machineRoot, `${sessionId}.json`),
40
+ };
41
+ }
42
+
43
+ export function createConversationId(agent: string, taskId: string): string {
44
+ return `${agent}-${taskId}`;
45
+ }
46
+
47
+ export function workflowToStage(workflow?: OrchestratorTask["workflow"]): VibeLifecycleStage | undefined {
48
+ switch (workflow) {
49
+ case "vibe-genesis":
50
+ return "genesis";
51
+ case "vibe-design":
52
+ return "design";
53
+ case "vibe-build":
54
+ return "build";
55
+ default:
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ function defaultStageForRole(role: TakomiRole): VibeLifecycleStage | undefined {
61
+ switch (role) {
62
+ case "architect":
63
+ return "genesis";
64
+ case "design":
65
+ return "design";
66
+ case "code":
67
+ case "review":
68
+ case "orchestrator":
69
+ return "build";
70
+ default:
71
+ return undefined;
72
+ }
73
+ }
74
+
75
+ function emptyStageState(): LifecycleStageState {
76
+ return {
77
+ status: "pending",
78
+ taskIds: [],
79
+ canExpand: true,
80
+ };
81
+ }
82
+
83
+ export function createEmptyLifecycle(): Record<VibeLifecycleStage, LifecycleStageState> {
84
+ return {
85
+ genesis: emptyStageState(),
86
+ design: emptyStageState(),
87
+ build: emptyStageState(),
88
+ };
89
+ }
90
+
91
+ function stageStatusFromTasks(tasks: OrchestratorTask[]): OrchestratorTaskStatus {
92
+ if (!tasks.length) return "pending";
93
+ if (tasks.every((task) => task.status === "completed")) return "completed";
94
+ if (tasks.some((task) => task.status === "in-progress")) return "in-progress";
95
+ if (tasks.some((task) => task.status === "blocked") && !tasks.some((task) => task.status === "completed")) return "blocked";
96
+ if (tasks.some((task) => task.status === "completed")) return "in-progress";
97
+ return "pending";
98
+ }
99
+
100
+ export function deriveLifecycleFromTasks(
101
+ tasks: OrchestratorTask[],
102
+ previous?: Partial<Record<VibeLifecycleStage, LifecycleStageState>>,
103
+ ): Record<VibeLifecycleStage, LifecycleStageState> {
104
+ const lifecycle = createEmptyLifecycle();
105
+
106
+ for (const stage of Object.keys(lifecycle) as VibeLifecycleStage[]) {
107
+ const stageTasks = tasks.filter((task) => task.stage === stage || workflowToStage(task.workflow) === stage);
108
+ const prev = previous?.[stage];
109
+ lifecycle[stage] = {
110
+ status: stageStatusFromTasks(stageTasks),
111
+ taskIds: stageTasks.map((task) => task.id),
112
+ canExpand: prev?.canExpand ?? true,
113
+ expandedAt: prev?.expandedAt,
114
+ notes: prev?.notes,
115
+ };
116
+ }
117
+
118
+ return lifecycle;
119
+ }
120
+
121
+ export function normalizeChecklist(checklist?: Array<string | TaskChecklistItem>): TaskChecklistItem[] | undefined {
122
+ if (!checklist?.length) return undefined;
123
+ return checklist.map((item) => typeof item === "string" ? { text: item, done: false } : { text: item.text, done: item.done ?? false });
124
+ }
125
+
126
+ export function createTask(id: string, title: string, role: TakomiRole, extras?: Partial<OrchestratorTask>): OrchestratorTask {
127
+ const preferredAgent = extras?.preferredAgent ?? (role === "design" ? "designer" : role === "architect" ? "architect" : role === "review" ? "reviewer" : role === "code" ? "coder" : "orchestrator");
128
+ const stage = extras?.stage ?? workflowToStage(extras?.workflow) ?? defaultStageForRole(role);
129
+ return {
130
+ id,
131
+ title,
132
+ role,
133
+ status: "pending",
134
+ ...extras,
135
+ stage,
136
+ preferredAgent,
137
+ conversationId: extras?.conversationId ?? createConversationId(preferredAgent, id),
138
+ preferredModel: extras?.preferredModel,
139
+ preferredModelHint: extras?.preferredModelHint,
140
+ preferredThinking: extras?.preferredThinking,
141
+ fallbackModels: extras?.fallbackModels,
142
+ dispatchPolicy: extras?.dispatchPolicy,
143
+ skills: extras?.skills,
144
+ checklist: normalizeChecklist(extras?.checklist),
145
+ };
146
+ }
147
+
148
+ export function getNextTaskId(tasks: OrchestratorTask[]): string {
149
+ const max = tasks.reduce((current, task) => {
150
+ const parsed = Number.parseInt(task.id, 10);
151
+ return Number.isFinite(parsed) ? Math.max(current, parsed) : current;
152
+ }, 0);
153
+ return String(max + 1).padStart(2, "0");
154
+ }
155
+
156
+ export function moveTaskStatus(tasks: OrchestratorTask[], id: string, status: OrchestratorTaskStatus): OrchestratorTask[] {
157
+ return tasks.map((task) => (task.id === id ? { ...task, status } : task));
158
+ }
159
+
160
+ function renderChecklist(checklist?: TaskChecklistItem[]): string[] {
161
+ if (!checklist?.length) return ["- No checklist yet."];
162
+ return checklist.map((item) => `- [${item.done ? "x" : " "}] ${item.text}`);
163
+ }
164
+
165
+ function renderBullets(items?: string[], empty = "- None specified."): string[] {
166
+ if (!items?.length) return [empty];
167
+ return items.map((item) => `- ${item}`);
168
+ }
169
+
170
+ function renderLifecycleSummary(state: OrchestratorSessionState): string[] {
171
+ return (Object.keys(state.lifecycle) as VibeLifecycleStage[]).flatMap((stage) => {
172
+ const entry = state.lifecycle[stage];
173
+ const taskSummary = entry.taskIds.length ? entry.taskIds.join(", ") : "none yet";
174
+ return [
175
+ `### ${stage[0].toUpperCase()}${stage.slice(1)}`,
176
+ `- Status: ${entry.status}`,
177
+ `- Tasks: ${taskSummary}`,
178
+ `- Expandable: ${entry.canExpand === false ? "no" : "yes"}`,
179
+ entry.expandedAt ? `- Expanded At: ${entry.expandedAt}` : "",
180
+ entry.notes ? `- Notes: ${entry.notes}` : "",
181
+ "",
182
+ ].filter(Boolean);
183
+ });
184
+ }
185
+
186
+ export function renderMasterPlan(sessionOrId: OrchestratorSessionState | string, title?: string, tasks?: OrchestratorTask[]): string {
187
+ const state = typeof sessionOrId === "string"
188
+ ? buildSessionState(sessionOrId, title ?? "Takomi Session", tasks ?? [])
189
+ : normalizeSessionState(sessionOrId);
190
+
191
+ const rows = state.tasks
192
+ .map((task) => `| ${task.id} | ${task.stage ?? "-"} | ${task.title} | ${task.status} | ${task.role} | ${task.preferredAgent ?? "-"} | ${task.workflow ?? "-"} | ${task.preferredModel ?? task.preferredModelHint ?? "-"} | ${task.preferredThinking ?? "-"} | ${task.dispatchPolicy ?? "-"} | ${task.skills?.join(", ") ?? "-"} |`)
193
+ .join("\n");
194
+
195
+ return [
196
+ `# Master Plan: ${state.title}`,
197
+ "",
198
+ `**Session ID:** ${state.sessionId}`,
199
+ `**Runtime Mode:** ${state.mode}`,
200
+ `**Session Intent:** ${state.sessionIntent ?? "full-project"}`,
201
+ "",
202
+ "## Lifecycle",
203
+ "",
204
+ ...renderLifecycleSummary(state),
205
+ "## Tasks",
206
+ "",
207
+ "| ID | Stage | Title | Status | Role | Preferred Agent | Workflow | Model | Thinking | Dispatch | Skills |",
208
+ "|---|---|---|---|---|---|---|---|---|---|---|",
209
+ rows || "| - | - | No tasks yet | - | - | - | - | - | - | - | - |",
210
+ "",
211
+ "## Notes",
212
+ "",
213
+ "- Human-readable task docs live in this session folder.",
214
+ "- Machine state lives in `.pi/takomi/orchestrator/<sessionId>.json`.",
215
+ "- Sending a task back to the same agent should reuse its conversationId when continuity is helpful.",
216
+ "- Sessions follow the Genesis -> Design -> Build lifecycle, but each stage may stay compact or expand into more tasks.",
217
+ ].join("\n");
218
+ }
219
+
220
+ export function renderTaskFile(task: OrchestratorTask, context?: string): string {
221
+ return [
222
+ `# Task: ${task.title}`,
223
+ "",
224
+ `**Task ID:** ${task.id}`,
225
+ `**Stage:** ${task.stage ?? "-"}`,
226
+ `**Status:** ${task.status}`,
227
+ `**Role:** ${task.role}`,
228
+ task.parentTaskId ? `**Parent Task:** ${task.parentTaskId}` : "",
229
+ `**Preferred Agent:** ${task.preferredAgent ?? "-"}`,
230
+ `**Conversation ID:** ${task.conversationId ?? "-"}`,
231
+ `**Workflow:** ${task.workflow ?? "-"}`,
232
+ task.preferredModel ? `**Model Override:** ${task.preferredModel}` : "",
233
+ task.preferredModelHint ? `**Model Hint:** ${task.preferredModelHint}` : "",
234
+ task.fallbackModels?.length ? `**Fallback Models:** ${task.fallbackModels.join(", ")}` : "",
235
+ task.preferredThinking ? `**Thinking Level:** ${task.preferredThinking}` : "",
236
+ task.dispatchPolicy ? `**Dispatch Policy:** ${task.dispatchPolicy}` : "",
237
+ task.skills?.length ? `**Required Skills:** ${task.skills.join(", ")}` : "",
238
+ "",
239
+ "## Context",
240
+ "",
241
+ context ?? "Add task-specific context here.",
242
+ "",
243
+ "## Objective",
244
+ "",
245
+ task.objective ?? task.title,
246
+ "",
247
+ "## Scope",
248
+ "",
249
+ ...renderBullets(task.scope),
250
+ "",
251
+ "## Checklist",
252
+ "",
253
+ ...renderChecklist(task.checklist),
254
+ "",
255
+ "## Definition of Done",
256
+ "",
257
+ ...renderBullets(task.definitionOfDone),
258
+ "",
259
+ "## Expected Artifacts",
260
+ "",
261
+ ...renderBullets(task.expectedArtifacts),
262
+ "",
263
+ "## Dependencies",
264
+ "",
265
+ ...renderBullets(task.dependencies),
266
+ "",
267
+ "## Review Checkpoint",
268
+ "",
269
+ task.reviewCheckpoint ?? "Review before implementation handoff or final completion.",
270
+ "",
271
+ "## Instructions",
272
+ "",
273
+ ...renderBullets(task.instructions ?? [
274
+ "complete the task within scope",
275
+ "use the listed workflow and skills when they are provided",
276
+ "report blockers clearly",
277
+ "if review sends this back, continue using the same conversation id when possible",
278
+ "summarize what changed and what remains",
279
+ ]),
280
+ task.notes ? "" : "",
281
+ task.notes ? "## Notes" : "",
282
+ task.notes ? "" : "",
283
+ task.notes ?? "",
284
+ ].filter(Boolean).join("\n");
285
+ }
286
+
287
+ export function buildSessionState(
288
+ sessionId: string,
289
+ title: string,
290
+ tasks: OrchestratorTask[],
291
+ now = new Date(),
292
+ extras?: Partial<Pick<OrchestratorSessionState, "sessionIntent" | "lifecycle">>,
293
+ ): OrchestratorSessionState {
294
+ const stamp = now.toISOString();
295
+ const normalizedTasks = tasks.map((task) => ({ ...task, stage: task.stage ?? workflowToStage(task.workflow) ?? defaultStageForRole(task.role) }));
296
+ return {
297
+ sessionId,
298
+ title,
299
+ createdAt: stamp,
300
+ updatedAt: stamp,
301
+ mode: "hybrid",
302
+ lifecycle: deriveLifecycleFromTasks(normalizedTasks, extras?.lifecycle),
303
+ sessionIntent: extras?.sessionIntent ?? "full-project",
304
+ tasks: normalizedTasks,
305
+ };
306
+ }
307
+
308
+ export function normalizeSessionState(
309
+ session: Partial<OrchestratorSessionState> & Pick<OrchestratorSessionState, "sessionId" | "title">,
310
+ ): OrchestratorSessionState {
311
+ const tasks = (session.tasks ?? []).map((task) => ({ ...task, stage: task.stage ?? workflowToStage(task.workflow) ?? defaultStageForRole(task.role) }));
312
+ const normalized = buildSessionState(
313
+ session.sessionId,
314
+ session.title,
315
+ tasks,
316
+ session.updatedAt ? new Date(session.updatedAt) : new Date(),
317
+ {
318
+ sessionIntent: session.sessionIntent ?? "full-project",
319
+ lifecycle: session.lifecycle,
320
+ },
321
+ );
322
+
323
+ return {
324
+ ...normalized,
325
+ createdAt: session.createdAt ?? normalized.createdAt,
326
+ updatedAt: session.updatedAt ?? normalized.updatedAt,
327
+ mode: "hybrid",
328
+ };
329
+ }
330
+
331
+ export function markStageExpanded(
332
+ state: OrchestratorSessionState,
333
+ stage: VibeLifecycleStage,
334
+ notes?: string,
335
+ now = new Date(),
336
+ ): OrchestratorSessionState {
337
+ const lifecycle = {
338
+ ...state.lifecycle,
339
+ [stage]: {
340
+ ...state.lifecycle[stage],
341
+ expandedAt: now.toISOString(),
342
+ notes: notes ?? state.lifecycle[stage].notes,
343
+ },
344
+ };
345
+ return normalizeSessionState({
346
+ ...state,
347
+ updatedAt: now.toISOString(),
348
+ lifecycle,
349
+ });
350
+ }
351
+
352
+ export function createLifecycleStarterSession(
353
+ title: string,
354
+ options?: { sessionId?: string; now?: Date; sessionIntent?: SessionIntent },
355
+ ): OrchestratorSessionState {
356
+ const sessionId = options?.sessionId ?? createSessionId(options?.now);
357
+ const tasks = [
358
+ createTask("01", "Genesis foundation", "orchestrator", {
359
+ stage: "genesis",
360
+ workflow: "vibe-genesis",
361
+ preferredAgent: "orchestrator",
362
+ objective: "Establish the project foundation, produce the required planning docs, and decide what should split next.",
363
+ scope: [
364
+ "Clarify scope and mission",
365
+ "Create or update the core markdown artifacts",
366
+ "Lock acceptance criteria and boundaries",
367
+ "Recommend whether Design and Build should stay compact or expand",
368
+ ],
369
+ checklist: [
370
+ { text: "Create or update requirements docs" },
371
+ { text: "Capture acceptance criteria" },
372
+ { text: "Define boundaries and non-goals" },
373
+ { text: "Recommend next-stage task breakdown" },
374
+ ],
375
+ definitionOfDone: [
376
+ "Required planning markdown files exist or are updated",
377
+ "Minimum usable state is explicit",
378
+ "Genesis recommends the correct next Design and Build structure",
379
+ ],
380
+ expectedArtifacts: [
381
+ "Requirements and feature docs",
382
+ "Genesis brief",
383
+ "Recommended task breakdown for later stages",
384
+ ],
385
+ reviewCheckpoint: "User or orchestrator approves the foundation before expanding later stages.",
386
+ instructions: [
387
+ "treat this as the root task for the whole Genesis -> Design -> Build lifecycle",
388
+ "create the required markdown artifacts before implementation begins",
389
+ "split later-stage work only when the scope justifies it",
390
+ "leave a clear recommendation for how Design and Build should fan out",
391
+ ],
392
+ }),
393
+ ];
394
+
395
+ return buildSessionState(sessionId, title, tasks, options?.now, {
396
+ sessionIntent: options?.sessionIntent ?? "full-project",
397
+ });
398
+ }
399
+
400
+ export function serializeSessionState(state: OrchestratorSessionState): string {
401
+ return `${JSON.stringify(normalizeSessionState(state), null, 2)}\n`;
402
+ }