takomi 2.1.44 → 2.1.45

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 (39) hide show
  1. package/.pi/README.md +4 -3
  2. package/.pi/extensions/oauth-router/commands.ts +66 -35
  3. package/.pi/extensions/oauth-router/index.ts +51 -7
  4. package/.pi/extensions/oauth-router/report-ui.ts +205 -0
  5. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
  6. package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
  7. package/.pi/extensions/takomi-context-manager/index.ts +3 -2
  8. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +36 -15
  9. package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
  10. package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
  11. package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
  12. package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
  13. package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
  14. package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
  15. package/.pi/extensions/takomi-context-manager/types.ts +6 -0
  16. package/.pi/extensions/takomi-runtime/commands.ts +45 -12
  17. package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
  18. package/.pi/extensions/takomi-runtime/index.ts +111 -42
  19. package/.pi/extensions/takomi-runtime/routing-policy.ts +14 -2
  20. package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
  21. package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
  22. package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
  23. package/.pi/extensions/takomi-subagents/agents.ts +4 -0
  24. package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
  25. package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
  26. package/.pi/extensions/takomi-subagents/index.ts +46 -1
  27. package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
  28. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +55 -46
  29. package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
  30. package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
  31. package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
  32. package/.pi/extensions/takomi-subagents/tool-runner.ts +158 -28
  33. package/.pi/takomi/model-routing.md +282 -3
  34. package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
  35. package/package.json +4 -3
  36. package/src/pi-takomi-core/types.ts +10 -0
  37. package/src/pi-takomi-core/workflows.ts +86 -45
  38. package/src/skills-catalog.js +2 -202
  39. package/src/skills-installer.js +4 -1
@@ -15,29 +15,47 @@ const pc = {
15
15
  magenta: ansi(35, 39),
16
16
  };
17
17
 
18
+ // USD per million tokens: input, cache read, output, cache write.
19
+ // OAuth-router's configured catalog is merged over these fallbacks at runtime,
20
+ // keeping Stats aligned with custom/new models without another hard-coded edit.
18
21
  const PRICES = {
19
- 'gpt-5.5': [5.00, 0.50, 30.00],
20
- 'gpt-5.4': [2.50, 0.25, 15.00],
21
- 'gpt-5.4-mini': [0.75, 0.075, 4.50],
22
- 'gpt-5.4-nano': [0.20, 0.02, 1.25],
23
- 'gpt-5.3-codex': [2.50, 0.25, 15.00],
24
- 'gpt-5.2-codex': [1.75, 0.175, 14.00],
25
- 'gpt-5-codex': [1.25, 0.125, 10.00],
26
- 'gpt-5.2': [1.75, 0.175, 14.00],
27
- 'gpt-5.1': [1.25, 0.125, 10.00],
28
- 'gpt-5': [1.25, 0.125, 10.00],
29
- 'gpt-5-mini': [0.25, 0.025, 2.00],
30
- 'gpt-4.1': [2.00, 0.50, 8.00],
31
- 'gpt-4o': [2.50, 1.25, 10.00],
32
- 'o4-mini': [1.10, 0.275, 4.40],
33
- 'claude-sonnet-4-6': [3.00, 0.30, 15.00],
22
+ 'gpt-5.6-luna': [1.00, 0.10, 6.00, 1.25],
23
+ 'gpt-5.6-sol': [5.00, 0.50, 30.00, 6.25],
24
+ 'gpt-5.6-terra': [2.50, 0.25, 15.00, 3.125],
25
+ 'gpt-5.5': [5.00, 0.50, 30.00, 6.25],
26
+ 'gpt-5.4': [2.50, 0.25, 15.00, 3.125],
27
+ 'gpt-5.4-mini': [0.75, 0.075, 4.50, 0.9375],
28
+ 'gpt-5.4-nano': [0.20, 0.02, 1.25, 0.25],
29
+ 'gpt-5.3-codex': [2.50, 0.25, 15.00, 3.125],
30
+ 'gpt-5.2-codex': [1.75, 0.175, 14.00, 2.1875],
31
+ 'gpt-5-codex': [1.25, 0.125, 10.00, 1.5625],
32
+ 'gpt-5.2': [1.75, 0.175, 14.00, 2.1875],
33
+ 'gpt-5.1': [1.25, 0.125, 10.00, 1.5625],
34
+ 'gpt-5': [1.25, 0.125, 10.00, 1.5625],
35
+ 'gpt-5-mini': [0.25, 0.025, 2.00, 0.3125],
36
+ 'gpt-4.1': [2.00, 0.50, 8.00, 2.00],
37
+ 'gpt-4o': [2.50, 1.25, 10.00, 2.50],
38
+ 'o4-mini': [1.10, 0.275, 4.40, 1.10],
39
+ 'claude-sonnet-4-6': [3.00, 0.30, 15.00, 3.75],
34
40
  };
35
41
 
36
42
  async function exists(target) { try { await fs.access(target); return true; } catch { return false; } }
37
43
  function safeJson(line) { try { return JSON.parse(line); } catch { return null; } }
38
44
  function dayOf(ts) { return typeof ts === 'string' && ts.length >= 10 ? ts.slice(0, 10) : 'unknown'; }
39
- function add(map, key, patch) { const row = map.get(key) || { key, input: 0, cache: 0, output: 0, total: 0, cost: 0, events: 0 }; for (const [k,v] of Object.entries(patch)) row[k] = (row[k] || 0) + (Number(v) || 0); if (!Object.prototype.hasOwnProperty.call(patch, 'events')) row.events += 1; map.set(key, row); }
40
- function cost(model, input, cache, output, additiveCache = true) { const p = PRICES[model]; if (!p) return 0; const nonCached = additiveCache ? input : Math.max(input - cache, 0); return (nonCached*p[0] + cache*p[1] + output*p[2]) / 1_000_000; }
45
+ function add(map, key, patch) { const row = map.get(key) || { key, input: 0, cache: 0, cacheWrite: 0, output: 0, total: 0, cost: 0, events: 0 }; for (const [k,v] of Object.entries(patch)) row[k] = (row[k] || 0) + (Number(v) || 0); if (!Object.prototype.hasOwnProperty.call(patch, 'events')) row.events += 1; map.set(key, row); }
46
+ function cost(model, input, cache, output, cacheWrite = 0, prices = PRICES) { const p = prices[model]; if (!p) return 0; return (input*p[0] + cache*p[1] + output*p[2] + cacheWrite*(p[3] ?? p[0])) / 1_000_000; }
47
+ async function loadPrices(home) {
48
+ const prices = { ...PRICES };
49
+ const configPath = path.join(home, '.pi', 'agent', 'oauth-router', 'config.json');
50
+ try {
51
+ const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
52
+ for (const model of config.models || []) {
53
+ if (!model?.id || !model?.cost) continue;
54
+ prices[model.id] = [model.cost.input || 0, model.cost.cacheRead || 0, model.cost.output || 0, model.cost.cacheWrite ?? model.cost.input ?? 0];
55
+ }
56
+ } catch {}
57
+ return prices;
58
+ }
41
59
  function fmtTokens(n) { if (n >= 1e9) return `${(n/1e9).toFixed(2)}B`; if (n >= 1e6) return `${(n/1e6).toFixed(1)}M`; if (n >= 1e3) return `${(n/1e3).toFixed(1)}K`; return String(Math.round(n || 0)); }
42
60
  function fmtMoney(n) { return `$${(n || 0).toFixed(n > 100 ? 0 : 2)}`; }
43
61
  function ms(n) { if (!n) return '-'; const s = Math.round(n/1000); if (s < 60) return `${s}s`; const m = Math.floor(s/60); if (m < 60) return `${m}m ${s%60}s`; const h = Math.floor(m/60); return `${h}h ${m%60}m`; }
@@ -117,7 +135,7 @@ function pushTask(taskRows, task) {
117
135
  taskRows.push(task);
118
136
  }
119
137
 
120
- async function scanPiSessions(root, source, events, sessionRows = [], taskRows = []) {
138
+ async function scanPiSessions(root, source, events, sessionRows = [], taskRows = [], prices = PRICES) {
121
139
  for (const file of await files(root)) {
122
140
  let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '', currentTask = null;
123
141
  const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0, activityTimestamps: [] };
@@ -170,7 +188,7 @@ async function scanPiSessions(root, source, events, sessionRows = [], taskRows =
170
188
  }
171
189
  }
172
190
  const u = msg && msg.usage;
173
- if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', input: +u.input||0, cache: +u.cacheRead||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0, true) });
191
+ if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', input: +u.input||0, cache: +u.cacheRead||0, cacheWrite: +u.cacheWrite||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0, +u.cacheWrite||0, prices) });
174
192
  }
175
193
  } catch {
176
194
  continue;
@@ -193,18 +211,19 @@ export async function collectTakomiStats(opts = {}) {
193
211
  const home = opts.home || os.homedir();
194
212
  const cwd = opts.cwd || process.cwd();
195
213
  const rawEvents = [], rawSessions = [], rawTasks = [];
214
+ const prices = await loadPrices(home);
196
215
  const globalSessions = path.resolve(path.join(home, '.pi', 'agent', 'sessions'));
197
216
  const projectSessions = path.resolve(path.join(cwd, '.pi', 'agent', 'sessions'));
198
- await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks);
199
- if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks);
200
- await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks);
217
+ await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks, prices);
218
+ if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks, prices);
219
+ await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks, prices);
201
220
  const sinceDay = parseSince(opts.since);
202
221
  const events = rawEvents.filter(e => !sinceDay || e.day >= sinceDay);
203
222
  const sessionRows = rawSessions.filter(s => !sinceDay || dayOf(s.end || s.start) >= sinceDay);
204
223
  const taskRows = rawTasks.filter(t => !sinceDay || dayOf(t.end || t.start) >= sinceDay);
205
224
  const runs = await scanRunHistory(path.join(home, '.pi', 'agent', 'run-history.jsonl'));
206
225
  const byDay = new Map(), byModel = new Map(), bySource = new Map(), byProject = new Map(), byTool = new Map(), byRole = new Map(), byStage = new Map(), byWorkflow = new Map();
207
- let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
226
+ let totals = { input: 0, cache: 0, cacheWrite: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
208
227
  for (const s of sessionRows) { totals.toolCalls += s.toolCalls; totals.turns += s.turns; }
209
228
  for (const e of events) {
210
229
  if (e.kind === 'tool') { add(byTool, e.tool || 'unknown', { total: 0, events: 1 }); continue; }
@@ -214,7 +233,7 @@ export async function collectTakomiStats(opts = {}) {
214
233
  add(byWorkflow, e.workflow || 'unknown', { total: 0, events: 1 });
215
234
  continue;
216
235
  }
217
- totals.input += e.input; totals.cache += e.cache; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
236
+ totals.input += e.input; totals.cache += e.cache; totals.cacheWrite += e.cacheWrite || 0; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
218
237
  add(byDay, e.day, e); add(byModel, e.model, e); add(bySource, e.source, e); add(byProject, e.project, e);
219
238
  }
220
239
  const byAgent = new Map(); let longestRun = null;
@@ -509,7 +528,8 @@ export function renderTakomiStats(stats, opts = {}) {
509
528
  // ── Stat Cards Row 1 ─────────────────────────────────────────────────
510
529
  const cards1 = [
511
530
  statCard(fmtTokens(stats.totals.total), 'Lifetime Tokens'),
512
- statCard(fmtTokens(stats.totals.cache), 'Cache Tokens'),
531
+ statCard(fmtTokens(stats.totals.cache), 'Cache Reads'),
532
+ statCard(fmtTokens(stats.totals.cacheWrite), 'Cache Writes'),
513
533
  statCard(fmtMoney(stats.totals.cost), 'Est. Cost'),
514
534
  statCard(String(stats.sessions), 'Sessions'),
515
535
  statCard(String(stats.totals.turns), 'Main Turns'),
@@ -677,7 +697,7 @@ export function renderTakomiStats(stats, opts = {}) {
677
697
  lines.push('');
678
698
  lines.push(' ' + pc.dim(hrule(W - 4)));
679
699
  lines.push(' ' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
680
- lines.push(' ' + pc.dim('Costs are estimates when provider prices are unknown.'));
700
+ lines.push(' ' + pc.dim('Costs use the oauth-router catalog when available; cache reads and writes are priced separately.'));
681
701
  lines.push('');
682
702
 
683
703
  return lines.join('\n');
@@ -0,0 +1,234 @@
1
+ import { getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent";
2
+ import { Container, Markdown, Text } from "@earendil-works/pi-tui";
3
+
4
+ type Theme = {
5
+ fg(color: string, text: string): string;
6
+ bold(text: string): string;
7
+ };
8
+
9
+ type ToolResult = {
10
+ content?: string | Array<{ type?: string; text?: string }>;
11
+ details?: unknown;
12
+ isError?: boolean;
13
+ };
14
+
15
+ type ResultOptions = { expanded: boolean; isError?: boolean };
16
+ type CardStatus = "success" | "warning" | "error" | "pending";
17
+
18
+ type ModeDetails = {
19
+ mode?: string;
20
+ source?: string;
21
+ reason?: string;
22
+ role?: string;
23
+ stage?: string;
24
+ workflow?: string;
25
+ };
26
+
27
+ type RoutingDetails = {
28
+ result?: { detectedDefaults?: string[] };
29
+ preview?: { scope?: string };
30
+ };
31
+
32
+ type WorkflowDetails = { id?: string; title?: string; purpose?: string };
33
+
34
+ type BoardError = {
35
+ code: string;
36
+ message: string;
37
+ severity: "warning" | "error";
38
+ };
39
+
40
+ type BoardDetails = {
41
+ sessionId?: string;
42
+ taskId?: string;
43
+ task?: { id?: string; status?: string };
44
+ tasks?: unknown[];
45
+ lifecycle?: unknown;
46
+ error?: BoardError;
47
+ };
48
+
49
+ type BoardArgs = {
50
+ action?: "init_session" | "expand_stage" | "show_workflows" | "show_session" | "update_task";
51
+ sessionId?: string;
52
+ taskId?: string;
53
+ stage?: string;
54
+ };
55
+
56
+ // Tool output is model-facing data. Sanitize only at the TUI boundary so
57
+ // ANSI, OSC, and C0 controls cannot alter terminal presentation.
58
+ function sanitizePresentation(value: string): string {
59
+ return value
60
+ .replace(/\x1B\][\s\S]*?(?:\x07|\x1B\\)/g, "") // OSC (BEL or ST terminated)
61
+ .replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "") // CSI
62
+ .replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, "");
63
+ }
64
+
65
+ function resultText(result: ToolResult): string {
66
+ const text = typeof result.content === "string"
67
+ ? result.content
68
+ : result.content?.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n") ?? "";
69
+ return sanitizePresentation(text);
70
+ }
71
+
72
+ function bounded(value: string | undefined, limit = 36): string {
73
+ const text = sanitizePresentation(value ?? "").replace(/\s+/g, " ").trim();
74
+ return text.length > limit ? `${text.slice(0, Math.max(1, limit - 1))}…` : text;
75
+ }
76
+
77
+ function status(result: ToolResult, options: ResultOptions, details?: { error?: BoardError }): CardStatus {
78
+ if (details?.error?.severity) return details.error.severity;
79
+ return result.isError || options.isError ? "error" : "success";
80
+ }
81
+
82
+ function renderToolCall(name: string, target: string | undefined, theme: Theme): Text {
83
+ return new Text(
84
+ `${theme.fg("toolTitle", theme.bold(`${name} `))}${target ? theme.fg("accent", bounded(target)) : ""}`,
85
+ 0,
86
+ 0,
87
+ );
88
+ }
89
+
90
+ function renderCompactCard(options: {
91
+ status: CardStatus;
92
+ title: string;
93
+ summary: string;
94
+ metadata: string;
95
+ }, theme: Theme): Text {
96
+ const presentation = {
97
+ success: ["✓", "success"],
98
+ warning: ["⚠", "warning"],
99
+ error: ["✗", "error"],
100
+ pending: ["…", "muted"],
101
+ } as const;
102
+ const [icon, color] = presentation[options.status];
103
+ return new Text([
104
+ `${theme.fg(color, icon)} ${theme.fg("accent", theme.bold(bounded(options.title)))} ${theme.fg("muted", bounded(options.summary, 72))}`,
105
+ theme.fg("dim", `${bounded(options.metadata, 64)} · ${keyHint("app.tools.expand", "view details")}`),
106
+ ].join("\n"), 0, 0);
107
+ }
108
+
109
+ function renderExpandedCard(options: {
110
+ status: CardStatus;
111
+ title: string;
112
+ summary: string;
113
+ metadata: string[];
114
+ markdown: string;
115
+ }, theme: Theme): Container {
116
+ const presentation = {
117
+ success: ["✓", "success"],
118
+ warning: ["⚠", "warning"],
119
+ error: ["✗", "error"],
120
+ pending: ["…", "muted"],
121
+ } as const;
122
+ const [icon, color] = presentation[options.status];
123
+ const container = new Container();
124
+ container.addChild(new Text(
125
+ `${theme.fg(color, icon)} ${theme.fg("accent", theme.bold(sanitizePresentation(options.title)))} ${theme.fg("muted", sanitizePresentation(options.summary))}`,
126
+ 0,
127
+ 0,
128
+ ));
129
+ for (const detail of options.metadata) container.addChild(new Text(theme.fg("dim", sanitizePresentation(detail)), 0, 0));
130
+ container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "collapse")), 0, 0));
131
+ container.addChild(new Markdown(sanitizePresentation(options.markdown), 0, 1, getMarkdownTheme()));
132
+ return container;
133
+ }
134
+
135
+ export function renderTakomiModeCall(args: { mode?: string } | undefined, theme: Theme): Text {
136
+ return renderToolCall("takomi_mode", args?.mode ?? "mode", theme);
137
+ }
138
+
139
+ export function renderTakomiModeResult(result: ToolResult, options: ResultOptions, theme: Theme): Container | Text {
140
+ const details = result.details as ModeDetails | undefined;
141
+ const primary = details?.stage ?? details?.role ?? details?.mode ?? "mode";
142
+ const source = details?.source ?? "model";
143
+ const metadata = [source, details?.workflow ? `wf:${details.workflow}` : "footer updated"].join(" · ");
144
+ if (!options.expanded) {
145
+ return renderCompactCard({ status: status(result, options), title: "Takomi mode", summary: `${source}:${primary}`, metadata }, theme);
146
+ }
147
+ return renderExpandedCard({
148
+ status: status(result, options),
149
+ title: "Takomi mode",
150
+ summary: `${source}:${primary}`,
151
+ metadata: [metadata],
152
+ markdown: resultText(result),
153
+ }, theme);
154
+ }
155
+
156
+ export function renderTakomiRoutingCall(args: { scope?: string } | undefined, theme: Theme): Text {
157
+ return renderToolCall("takomi_apply_routing_policy", args?.scope ?? "global", theme);
158
+ }
159
+
160
+ export function renderTakomiRoutingResult(result: ToolResult, options: ResultOptions, theme: Theme): Container | Text {
161
+ const details = result.details as RoutingDetails | undefined;
162
+ const scope = details?.preview?.scope ?? "global";
163
+ const detectedCount = details?.result?.detectedDefaults?.length ?? 0;
164
+ const summary = status(result, options) === "error" ? "policy was not saved" : `${scope} policy saved`;
165
+ const metadata = `${detectedCount} routing defaults detected`;
166
+ if (!options.expanded) return renderCompactCard({ status: status(result, options), title: "Takomi routing", summary, metadata }, theme);
167
+ return renderExpandedCard({
168
+ status: status(result, options),
169
+ title: "Takomi routing",
170
+ summary,
171
+ metadata: [`Scope: ${scope}`, metadata],
172
+ markdown: resultText(result),
173
+ }, theme);
174
+ }
175
+
176
+ export function renderTakomiWorkflowCall(args: { workflow?: string } | undefined, theme: Theme): Text {
177
+ return renderToolCall("takomi_workflow", args?.workflow ?? "library", theme);
178
+ }
179
+
180
+ export function renderTakomiWorkflowResult(result: ToolResult, options: ResultOptions, theme: Theme): Container | Text {
181
+ const details = result.details as WorkflowDetails | undefined;
182
+ const selected = details?.id;
183
+ const title = selected ? "Takomi workflow" : "Workflow library";
184
+ const summary = selected ? (details?.title ?? selected) : "embedded lifecycle playbooks";
185
+ const metadata = selected ? bounded(selected) : "genesis · design · build";
186
+ if (!options.expanded) return renderCompactCard({ status: status(result, options), title, summary, metadata }, theme);
187
+ return renderExpandedCard({ status: status(result, options), title, summary, metadata: [metadata], markdown: resultText(result) }, theme);
188
+ }
189
+
190
+ function boardPresentation(args: BoardArgs, details: BoardDetails | undefined): { title: string; summary: string; metadata: string } {
191
+ switch (args.action) {
192
+ case "show_workflows":
193
+ return { title: "Workflow library", summary: "available lifecycle playbooks", metadata: "genesis · design · build" };
194
+ case "show_session":
195
+ return { title: "Takomi session", summary: "session loaded", metadata: `session:${bounded(args.sessionId ?? details?.sessionId, 24)}` };
196
+ case "update_task":
197
+ return {
198
+ title: `Task ${bounded(args.taskId ?? details?.taskId ?? details?.task?.id, 24) || "update"}`,
199
+ summary: details?.task?.status ? `status: ${details.task.status}` : "task updated",
200
+ metadata: `session:${bounded(args.sessionId ?? details?.sessionId, 24)}`,
201
+ };
202
+ case "expand_stage":
203
+ return {
204
+ title: `${args.stage ?? "Lifecycle"} stage`,
205
+ summary: "stage expanded",
206
+ metadata: `session:${bounded(args.sessionId ?? details?.sessionId, 24)}`,
207
+ };
208
+ default:
209
+ return {
210
+ title: "Takomi board",
211
+ summary: "orchestrator session created",
212
+ metadata: `session:${bounded(details?.sessionId ?? args.sessionId, 24)}`,
213
+ };
214
+ }
215
+ }
216
+
217
+ export function renderTakomiBoardCall(args: BoardArgs | undefined, theme: Theme): Text {
218
+ return renderToolCall("takomi_board", args?.action ?? "init_session", theme);
219
+ }
220
+
221
+ export function renderTakomiBoardResult(result: ToolResult, options: ResultOptions, theme: Theme, args?: BoardArgs): Container | Text {
222
+ const details = result.details as BoardDetails | undefined;
223
+ const presentation = boardPresentation(args ?? {}, details);
224
+ const cardStatus = status(result, options, details);
225
+ const summary = cardStatus === "success" ? presentation.summary : details?.error?.message ?? "action needs attention";
226
+ if (!options.expanded) return renderCompactCard({ status: cardStatus, title: presentation.title, summary, metadata: presentation.metadata }, theme);
227
+ return renderExpandedCard({
228
+ status: cardStatus,
229
+ title: presentation.title,
230
+ summary,
231
+ metadata: [presentation.metadata],
232
+ markdown: resultText(result),
233
+ }, theme);
234
+ }
@@ -0,0 +1,54 @@
1
+ import {
2
+ getWorkflowDefinition,
3
+ listWorkflowCatalog,
4
+ type TakomiWorkflowId,
5
+ type WorkflowCatalogEntry,
6
+ type WorkflowDefinition,
7
+ } from "../../../src/pi-takomi-core";
8
+
9
+ type WorkflowCatalogPayload = {
10
+ content: Array<{ type: "text"; text: string }>;
11
+ details: { workflows: readonly WorkflowCatalogEntry[] };
12
+ };
13
+
14
+ type WorkflowPlaybookPayload = {
15
+ content: Array<{ type: "text"; text: string }>;
16
+ details: WorkflowDefinition & { workflows: readonly WorkflowCatalogEntry[] };
17
+ };
18
+
19
+ function formatCatalogEntry(workflow: WorkflowCatalogEntry): string {
20
+ return `${workflow.id} (${workflow.stage}) — ${workflow.name}: ${workflow.description} [${workflow.availability}]`;
21
+ }
22
+
23
+ /** Discovery/load adapter for the direct workflow API. */
24
+ export function discoverWorkflowPlaybooks(workflowId?: TakomiWorkflowId): WorkflowPlaybookPayload | WorkflowCatalogPayload {
25
+ if (workflowId) {
26
+ const workflow = getWorkflowDefinition(workflowId);
27
+ return {
28
+ content: [{ type: "text", text: `${workflow.title}\n\n${workflow.playbook}` }],
29
+ details: { ...workflow, workflows: listWorkflowCatalog() },
30
+ };
31
+ }
32
+
33
+ const workflows = listWorkflowCatalog();
34
+ return {
35
+ content: [{ type: "text", text: workflows.map(formatCatalogEntry).join("\n") }],
36
+ details: { workflows },
37
+ };
38
+ }
39
+
40
+ /** Lifecycle-board adapter; intentionally catalog-oriented and playbook-free. */
41
+ export function showWorkflowCatalogForBoard(): WorkflowCatalogPayload {
42
+ const workflows = listWorkflowCatalog();
43
+ return {
44
+ content: [{
45
+ type: "text",
46
+ text: [
47
+ "Takomi lifecycle workflow catalog",
48
+ "",
49
+ ...workflows.map((workflow) => `- ${formatCatalogEntry(workflow)}`),
50
+ ].join("\n"),
51
+ }],
52
+ details: { workflows },
53
+ };
54
+ }
@@ -12,6 +12,7 @@ export type TakomiAgentConfig = {
12
12
  model?: string;
13
13
  fallbackModels?: string[];
14
14
  thinking?: TakomiThinkingLevel;
15
+ defaultContext?: "fresh" | "fork";
15
16
  systemPrompt: string;
16
17
  filePath: string;
17
18
  source: "user" | "project";
@@ -59,6 +60,9 @@ function loadAgentsFromDirectory(agentsDir: string, source: "user" | "project"):
59
60
  model: frontmatter.model,
60
61
  fallbackModels: splitList(frontmatter.fallbackModels ?? frontmatter.fallback_models),
61
62
  thinking: normalizeThinking(frontmatter.thinking),
63
+ defaultContext: frontmatter.defaultContext === "fork" || frontmatter.defaultContext === "fresh"
64
+ ? frontmatter.defaultContext
65
+ : undefined,
62
66
  systemPrompt: body,
63
67
  filePath,
64
68
  source,