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
@@ -0,0 +1,112 @@
1
+ import { getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent";
2
+ import { Container, Markdown, Text, type Component } 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?: Array<{ type?: string; text?: string }>;
11
+ };
12
+
13
+ const OSC_SEQUENCE = /\x1B\][\s\S]*?(?:\x07|\x1B\\|$)/g;
14
+ const STRING_TERMINATED_SEQUENCE = /\x1B[PX^_][\s\S]*?(?:\x1B\\|$)/g;
15
+ const CSI_SEQUENCE = /(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]/g;
16
+ const ESC_SEQUENCE = /\x1B(?:[()][0-2A-Z0-9]|[=>]|[ -/]*[@-~]?)/g;
17
+ const UNSAFE_CONTROLS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g;
18
+
19
+ /**
20
+ * Remove terminal controls at the TUI boundary while retaining Markdown's
21
+ * printable syntax and line structure. Tool result content remains unchanged.
22
+ */
23
+ export function sanitizePresentation(value: string): string {
24
+ return value
25
+ .replace(OSC_SEQUENCE, "")
26
+ .replace(STRING_TERMINATED_SEQUENCE, "")
27
+ .replace(CSI_SEQUENCE, "")
28
+ .replace(ESC_SEQUENCE, "")
29
+ .replace(UNSAFE_CONTROLS, "");
30
+ }
31
+
32
+ /** Read model-facing content without modifying it, then make a presentation-safe copy. */
33
+ export function resultText(result: ToolResult): string {
34
+ const text = result.content?.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n") ?? "";
35
+ return sanitizePresentation(text);
36
+ }
37
+
38
+ export function renderToolCall(toolName: string, target: string | undefined, theme: Theme): Text {
39
+ const safeToolName = sanitizePresentation(toolName);
40
+ const safeTarget = target ? sanitizePresentation(target) : "";
41
+ return new Text(
42
+ `${theme.fg("toolTitle", theme.bold(`${safeToolName} `))}${safeTarget ? theme.fg("accent", safeTarget) : ""}`,
43
+ 0,
44
+ 0,
45
+ );
46
+ }
47
+
48
+ export function renderCompactCard(options: {
49
+ status: "success" | "warning" | "error" | "pending";
50
+ title: string;
51
+ summary: string;
52
+ metadata?: string;
53
+ /** Items render inline on wide terminals and stack below 80 columns. */
54
+ responsiveMetadata?: string[];
55
+ }, theme: Theme): Component {
56
+ const status = {
57
+ success: ["✓", "success"],
58
+ warning: ["⚠", "warning"],
59
+ error: ["✗", "error"],
60
+ pending: ["…", "muted"],
61
+ } as const;
62
+ const [icon, color] = status[options.status];
63
+ const title = sanitizePresentation(options.title);
64
+ const summary = sanitizePresentation(options.summary);
65
+ const metadata = options.metadata ? sanitizePresentation(options.metadata) : undefined;
66
+ const responsiveMetadata = options.responsiveMetadata?.map(sanitizePresentation).filter(Boolean);
67
+ const header = `${theme.fg(color, icon)} ${theme.fg("accent", theme.bold(title))} ${theme.fg("muted", summary)}`;
68
+ const hint = keyHint("app.tools.expand", "view details");
69
+
70
+ if (!responsiveMetadata?.length) {
71
+ const detail = metadata ? `${metadata} · ${hint}` : hint;
72
+ return new Text([header, theme.fg("dim", detail)].join("\n"), 0, 0);
73
+ }
74
+
75
+ return {
76
+ invalidate() {},
77
+ render(width: number): string[] {
78
+ const details = width < 80
79
+ ? [...responsiveMetadata.map((item) => theme.fg("dim", item)), theme.fg("dim", hint)]
80
+ : [theme.fg("dim", `${responsiveMetadata.join(" · ")} · ${hint}`)];
81
+ return new Text([header, ...details].join("\n"), 0, 0).render(width);
82
+ },
83
+ };
84
+ }
85
+
86
+ export function renderExpandedMarkdown(options: {
87
+ status: "success" | "warning" | "error" | "pending";
88
+ title: string;
89
+ summary: string;
90
+ metadata?: string[];
91
+ markdown: string;
92
+ }, theme: Theme): Container {
93
+ const status = {
94
+ success: ["✓", "success"],
95
+ warning: ["⚠", "warning"],
96
+ error: ["✗", "error"],
97
+ pending: ["…", "muted"],
98
+ } as const;
99
+ const [icon, color] = status[options.status];
100
+ const title = sanitizePresentation(options.title);
101
+ const summary = sanitizePresentation(options.summary);
102
+ const container = new Container();
103
+ container.addChild(new Text(
104
+ `${theme.fg(color, icon)} ${theme.fg("accent", theme.bold(title))} ${theme.fg("muted", summary)}`,
105
+ 0,
106
+ 0,
107
+ ));
108
+ for (const detail of options.metadata ?? []) container.addChild(new Text(theme.fg("dim", sanitizePresentation(detail)), 0, 0));
109
+ container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "collapse")), 0, 0));
110
+ container.addChild(new Markdown(sanitizePresentation(options.markdown), 0, 1, getMarkdownTheme()));
111
+ return container;
112
+ }
@@ -2,6 +2,12 @@ export type SkillRecord = {
2
2
  name: string;
3
3
  description?: string;
4
4
  location?: string;
5
+ /** Explicit skill metadata, preferred over all other category sources. */
6
+ category?: string;
7
+ /** Category supplied by an installer/source registry for this exact skill path. */
8
+ sourceCategory?: string;
9
+ /** Package/source identifier used only when no explicit, registry, or path category exists. */
10
+ packageName?: string;
5
11
  source: "systemPromptOptions" | "xml" | "filesystem" | "tool";
6
12
  };
7
13
 
@@ -28,6 +28,7 @@ export type TakomiRuntimeCommandState = {
28
28
  type RegisterTakomiCommandOptions = {
29
29
  getState(): TakomiRuntimeCommandState;
30
30
  updateState(ctx: ExtensionContext, mutator: () => void, message?: string | (() => string)): Promise<void>;
31
+ recordUserGateAutoProvenance(authorized: boolean): void;
31
32
  resetRuntime(ctx: ExtensionCommandContext): Promise<void>;
32
33
  setStageAndWorkflow(stage: VibeLifecycleStage, options?: { preserveRole?: boolean }): void;
33
34
  createPlanSession(ctx: ExtensionCommandContext, title?: string): Promise<string>;
@@ -36,6 +37,13 @@ type RegisterTakomiCommandOptions = {
36
37
  };
37
38
 
38
39
  export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomiCommandOptions): void {
40
+ // The custom runtime widget is intentionally absent in idle/direct mode, so
41
+ // suppress routine notifications only while the changed state remains visible.
42
+ function hasVisibleRuntimeWidget(): boolean {
43
+ const state = options.getState();
44
+ return state.enabled && (state.modeSource ?? "idle") !== "idle";
45
+ }
46
+
39
47
  async function handleStage(ctx: ExtensionCommandContext, stage: VibeLifecycleStage, prompt?: string): Promise<void> {
40
48
  await options.updateState(ctx, () => {
41
49
  options.getState().enabled = true;
@@ -75,26 +83,30 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
75
83
  state.autoOrch = false;
76
84
  state.planMode = true;
77
85
  state.launchMode = "manual";
86
+ options.recordUserGateAutoProvenance(false);
78
87
  state.role = "review";
79
88
  state.stage = undefined;
80
89
  state.workflow = undefined;
81
90
  state.modeSource = "manual";
82
91
  state.modeReason = "/takomi mode review";
83
92
  }
84
- }, () => `Takomi mode set to ${mode}`);
93
+ }, () => hasVisibleRuntimeWidget() ? "" : `Takomi mode set to ${mode}`);
85
94
  }
86
95
 
87
96
  async function handleGate(ctx: ExtensionCommandContext, gate?: string): Promise<void> {
88
- if (gate !== "auto" && gate !== "review") {
89
- ctx.ui.notify("Usage: /takomi gate <auto|review>", "warning");
97
+ if (gate !== "auto" && gate !== "review" && gate !== "manual") {
98
+ ctx.ui.notify("Usage: /takomi gate <auto|review|manual>", "warning");
90
99
  return;
91
100
  }
101
+ const auto = gate === "auto";
92
102
  await options.updateState(ctx, () => {
93
103
  const state = options.getState();
94
104
  state.enabled = true;
95
- state.launchMode = gate === "review" ? "manual" : "auto";
96
- state.autoOrch = gate === "auto";
97
- }, () => `Takomi execution gate set to ${gate}`);
105
+ state.launchMode = auto ? "auto" : "manual";
106
+ state.autoOrch = auto;
107
+ // This dedicated entry is the only user-gate authorization signal.
108
+ options.recordUserGateAutoProvenance(auto);
109
+ }, () => hasVisibleRuntimeWidget() ? "" : `Takomi execution gate set to ${auto ? "auto" : "review"}`);
98
110
  }
99
111
 
100
112
  async function handleRouting(ctx: ExtensionCommandContext, body?: string): Promise<void> {
@@ -173,20 +185,41 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
173
185
  const policyText = scopeMatch?.[2] ?? trimmed.replace(/^set\s+/i, "");
174
186
 
175
187
  try {
176
- const preview = previewTakomiRoutingPolicy(ctx.cwd, policyText, { scope });
188
+ const availableModels = (() => {
189
+ try {
190
+ const available = (ctx as ExtensionCommandContext & { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } }).modelRegistry?.getAvailable?.() ?? [];
191
+ return available.map((model) => `${model.provider ? `${model.provider}/` : ""}${model.id ?? model.name ?? ""}`).filter(Boolean);
192
+ } catch {
193
+ return [];
194
+ }
195
+ })();
196
+ const [preview, activePolicy] = await Promise.all([
197
+ Promise.resolve(previewTakomiRoutingPolicy(ctx.cwd, policyText, { scope, availableModels })),
198
+ resolveTakomiRoutingPolicy(ctx.cwd),
199
+ ]);
200
+ const replacementWarning = activePolicy.text && activePolicy.text.trim().length > preview.policy.trim().length * 2
201
+ ? `WARNING: This input is much shorter than the active policy (${preview.policy.length} vs ${activePolicy.text.length} characters). It will replace the file, not merge into it. If the user referred to a full source file or prior policy, inspect and use that complete text instead.`
202
+ : "This input replaces the selected policy file exactly; it is not merged with the active policy.";
177
203
  const reviewPrompt = [
178
204
  "Review this Takomi routing policy extraction before it is saved.",
179
205
  "",
180
206
  "Rules:",
181
207
  "- Do not invent providers or model IDs not grounded in the policy.",
182
- "- Providerless names like GPT-5.5 are routing intent unless a preferred provider/router is declared.",
183
- "- Valid Takomi roles are: general, orchestrator, architect, designer, coder, reviewer.",
184
- "- If the extraction is correct and safe, call takomi_apply_routing_policy with the exact policyText and scope below.",
185
- "- If it is ambiguous or wrong, explain what the user should clarify and do not call the tool.",
208
+ "- Providerless names are valid routing intent. Require a provider only when writing an executable model override.",
209
+ "- Check the available Pi model registry below before asking the user whether a named model exists or what its exact ID is.",
210
+ "- Conditional task-shape routes do not need to be forced into role-wide defaults or agentOverrides.",
211
+ "- Valid role-wide Takomi overrides are: general, orchestrator, architect, designer, coder, reviewer. Other headings may still be policy concepts or execution routes.",
212
+ "- Preserve the user's full authored policy. If this looks like a summary/excerpt and a richer referenced source exists, inspect that source and apply the complete intended text instead of overwriting it with the excerpt.",
213
+ "- If the extraction is correct and safe, call takomi_apply_routing_policy with the complete intended policyText and scope below.",
214
+ "- Ask only for unresolved provider/account choices or genuine policy ambiguity; do not ask for facts available from the registry or files.",
186
215
  "",
187
216
  "Deterministic extraction:",
188
217
  renderRoutingPolicyPreview(preview),
189
218
  "",
219
+ replacementWarning,
220
+ "",
221
+ availableModels.length ? `Available Pi models:\n${availableModels.map((model) => `- ${model}`).join("\n")}` : "Available Pi models: registry unavailable; inspect it before asking the user if possible.",
222
+ "",
190
223
  "Original policy text:",
191
224
  "```",
192
225
  preview.policy,
@@ -217,7 +250,7 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
217
250
  if (action === "on" || action === "off") {
218
251
  await options.updateState(ctx, () => {
219
252
  options.getState().subagentsEnabled = action === "on";
220
- }, `Takomi subagents ${action}`);
253
+ }, () => hasVisibleRuntimeWidget() ? "" : `Takomi subagents ${action}`);
221
254
  return;
222
255
  }
223
256
  if (action === "status" || !action) {
@@ -0,0 +1,24 @@
1
+ export const USER_GATE_AUTO_PROVENANCE_ENTRY = "takomi-user-gate-auto-provenance";
2
+
3
+ type SessionEntry = {
4
+ type?: unknown;
5
+ customType?: unknown;
6
+ data?: unknown;
7
+ };
8
+
9
+ function isAuthorizedData(value: unknown): value is { authorized: true } {
10
+ return typeof value === "object" && value !== null && (value as { authorized?: unknown }).authorized === true;
11
+ }
12
+
13
+ /**
14
+ * Reads the latest explicit user gate decision from session history.
15
+ * This deliberately ignores generic runtime state and profile defaults.
16
+ */
17
+ export function hasUserGateAutoProvenance(entries: readonly SessionEntry[]): boolean {
18
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
19
+ const entry = entries[index];
20
+ if (entry?.type !== "custom" || entry.customType !== USER_GATE_AUTO_PROVENANCE_ENTRY) continue;
21
+ return isAuthorizedData(entry.data);
22
+ }
23
+ return false;
24
+ }
@@ -13,7 +13,6 @@ import {
13
13
  getSessionPaths,
14
14
  getNextTaskId,
15
15
  getWorkflowDefinition,
16
- listWorkflowDefinitions,
17
16
  markStageExpanded,
18
17
  normalizeSessionState,
19
18
  renderMasterPlan,
@@ -50,12 +49,27 @@ import {
50
49
  } from "./shared";
51
50
  import { TakomiContextPanel, wireContextPanel } from "./context-panel";
52
51
  import { registerTakomiCommands } from "./commands";
52
+ import { USER_GATE_AUTO_PROVENANCE_ENTRY } from "./gate-provenance";
53
53
  import {
54
54
  DEFAULT_TAKOMI_PROFILE,
55
55
  getProfileDefaults,
56
56
  loadTakomiProfile,
57
57
  } from "./profile";
58
58
  import { installTakomiRoutingPolicy, previewTakomiRoutingPolicy, renderRoutingPolicyPreview, resolveTakomiRoutingPolicy } from "./routing-policy";
59
+ import {
60
+ discoverWorkflowPlaybooks,
61
+ showWorkflowCatalogForBoard,
62
+ } from "./workflow-catalog";
63
+ import {
64
+ renderTakomiBoardCall,
65
+ renderTakomiBoardResult,
66
+ renderTakomiModeCall,
67
+ renderTakomiModeResult,
68
+ renderTakomiRoutingCall,
69
+ renderTakomiRoutingResult,
70
+ renderTakomiWorkflowCall,
71
+ renderTakomiWorkflowResult,
72
+ } from "./tool-renderers";
59
73
 
60
74
  type TakomiModeSource = "idle" | "manual" | "model" | "board";
61
75
 
@@ -399,6 +413,24 @@ function getIncompleteChecklistItems(checklist?: OrchestratorTask["checklist"]):
399
413
  .map((item) => item.text);
400
414
  }
401
415
 
416
+ type BoardErrorSeverity = "warning" | "error";
417
+
418
+ function createBoardErrorResult(
419
+ text: string,
420
+ code: string,
421
+ severity: BoardErrorSeverity,
422
+ details: Record<string, unknown> = {},
423
+ ) {
424
+ return {
425
+ content: [{ type: "text" as const, text }],
426
+ // Keep this semantic error independent of Pi's transport-level isError.
427
+ // Renderers use it to retain warning/error meaning even when Pi does not
428
+ // pass top-level result flags back into renderResult.
429
+ details: { ...details, error: { code, message: text, severity } },
430
+ isError: true,
431
+ };
432
+ }
433
+
402
434
  function getCompletionGateError(task: Pick<OrchestratorTask, "id" | "title" | "checklist">): string | undefined {
403
435
  if (!task.checklist?.length) {
404
436
  return `Task ${task.id} cannot be marked completed until it has a checklist.`;
@@ -688,6 +720,10 @@ function installTakomiFooter(ctx: ExtensionContext, stateRef: { current: TakomiS
688
720
  ctx.ui.setFooter((tui, theme, footerData) => new TakomiFooterComponent(tui, theme, footerData, ctx, () => stateRef.current));
689
721
  }
690
722
 
723
+ function hasVisibleRuntimeWidget(state: TakomiState): boolean {
724
+ return state.enabled && (state.modeSource ?? "idle") !== "idle";
725
+ }
726
+
691
727
  async function refreshUi(
692
728
  ctx: ExtensionContext,
693
729
  state: TakomiState,
@@ -742,6 +778,12 @@ export default function takomiRuntime(pi: ExtensionAPI) {
742
778
  pi.appendEntry(STATE_ENTRY, state);
743
779
  }
744
780
 
781
+ // This is intentionally separate from generic runtime-state persistence.
782
+ // Only user slash-command handlers receive the recorder below.
783
+ function recordUserGateAutoProvenance(authorized: boolean): void {
784
+ pi.appendEntry(USER_GATE_AUTO_PROVENANCE_ENTRY, { authorized });
785
+ }
786
+
745
787
  function syncContextPanelState() {
746
788
  contextPanel.setRuntimeState({
747
789
  role: state.role,
@@ -829,6 +871,7 @@ export default function takomiRuntime(pi: ExtensionAPI) {
829
871
  registerTakomiCommands(pi, {
830
872
  getState: () => state,
831
873
  updateState,
874
+ recordUserGateAutoProvenance,
832
875
  setStageAndWorkflow: (stage, options) => setStageAndWorkflow(state, stage, options),
833
876
  hasGenesisArtifacts,
834
877
  subagentController,
@@ -857,12 +900,13 @@ export default function takomiRuntime(pi: ExtensionAPI) {
857
900
  },
858
901
  resetRuntime: async (ctx) => {
859
902
  await updateState(ctx, () => {
903
+ recordUserGateAutoProvenance(false);
860
904
  state = cloneState(DEFAULT_STATE);
861
905
  activeSubagentLabel = undefined;
862
906
  activeSubagentAgent = undefined;
863
907
  activeSubagentTask = undefined;
864
908
  activeSubagentStatus = undefined;
865
- }, "Takomi runtime state reset");
909
+ }, () => hasVisibleRuntimeWidget(state) ? "" : "Takomi runtime state reset");
866
910
  subagentController.reset(ctx);
867
911
  contextPanel.resetSession();
868
912
  contextPanel.show(ctx);
@@ -920,7 +964,9 @@ export default function takomiRuntime(pi: ExtensionAPI) {
920
964
  syncContextPanelState();
921
965
  await refreshUi(ctx, state, footerStateRef);
922
966
  const label = state.modeSource === "idle" ? "idle" : `${state.modeSource}:${state.stage ?? state.role}`;
923
- return `Takomi mode set to ${label}${state.modeReason ? ` (${state.modeReason})` : ""}.`;
967
+ const text = `Takomi mode set to ${label}${state.modeReason ? ` (${state.modeReason})` : ""}.`;
968
+ if (!hasVisibleRuntimeWidget(state)) ctx.ui.notify(text, "info");
969
+ return text;
924
970
  }
925
971
 
926
972
  pi.registerTool({
@@ -939,12 +985,13 @@ export default function takomiRuntime(pi: ExtensionAPI) {
939
985
  }),
940
986
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
941
987
  const text = await applyTakomiMode(ctx, params.mode, "model", params.reason);
942
- ctx.ui.notify(text, "info");
943
988
  return {
944
989
  content: [{ type: "text", text }],
945
990
  details: { mode: params.mode, source: state.modeSource, reason: state.modeReason, role: state.role, stage: state.stage, workflow: state.workflow },
946
991
  };
947
992
  },
993
+ renderCall: renderTakomiModeCall,
994
+ renderResult: (result, options, theme) => renderTakomiModeResult(result, options, theme),
948
995
  });
949
996
 
950
997
  pi.registerTool({
@@ -963,7 +1010,15 @@ export default function takomiRuntime(pi: ExtensionAPI) {
963
1010
  }),
964
1011
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
965
1012
  const scope = params.scope ?? "global";
966
- const preview = previewTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
1013
+ const availableModels = (() => {
1014
+ try {
1015
+ const available = (ctx as typeof ctx & { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } }).modelRegistry?.getAvailable?.() ?? [];
1016
+ return available.map((model) => `${model.provider ? `${model.provider}/` : ""}${model.id ?? model.name ?? ""}`).filter(Boolean);
1017
+ } catch {
1018
+ return [];
1019
+ }
1020
+ })();
1021
+ const preview = previewTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope, availableModels });
967
1022
  const result = await installTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
968
1023
  const scopeNote = scope === "global"
969
1024
  ? "This global policy applies unless a project-local override exists."
@@ -986,6 +1041,8 @@ export default function takomiRuntime(pi: ExtensionAPI) {
986
1041
  details: { result, preview, reviewNotes: params.reviewNotes },
987
1042
  };
988
1043
  },
1044
+ renderCall: renderTakomiRoutingCall,
1045
+ renderResult: (result, options, theme) => renderTakomiRoutingResult(result, options, theme),
989
1046
  });
990
1047
 
991
1048
  pi.registerTool({
@@ -997,20 +1054,10 @@ export default function takomiRuntime(pi: ExtensionAPI) {
997
1054
  workflow: Type.Optional(StringEnum(["vibe-genesis", "vibe-design", "vibe-build"] as const)),
998
1055
  }),
999
1056
  async execute(_toolCallId, params) {
1000
- if (params.workflow) {
1001
- const workflow = getWorkflowDefinition(params.workflow);
1002
- return {
1003
- content: [{ type: "text", text: `${workflow.title}\n\n${workflow.playbook}` }],
1004
- details: workflow,
1005
- };
1006
- }
1007
-
1008
- const workflows = listWorkflowDefinitions();
1009
- return {
1010
- content: [{ type: "text", text: workflows.map((workflow) => `${workflow.id}: ${workflow.purpose}`).join("\n") }],
1011
- details: undefined,
1012
- };
1057
+ return discoverWorkflowPlaybooks(params.workflow);
1013
1058
  },
1059
+ renderCall: renderTakomiWorkflowCall,
1060
+ renderResult: (result, options, theme) => renderTakomiWorkflowResult(result, options, theme),
1014
1061
  });
1015
1062
 
1016
1063
  pi.registerTool({
@@ -1080,16 +1127,12 @@ export default function takomiRuntime(pi: ExtensionAPI) {
1080
1127
  }),
1081
1128
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1082
1129
  if (params.action === "show_workflows") {
1083
- const workflows = listWorkflowDefinitions();
1084
- return {
1085
- content: [{ type: "text", text: workflows.map((workflow) => `${workflow.id}: ${workflow.playbook}`).join("\n\n") }],
1086
- details: { workflows },
1087
- };
1130
+ return showWorkflowCatalogForBoard();
1088
1131
  }
1089
1132
 
1090
1133
  if (params.action === "show_session") {
1091
1134
  if (!params.sessionId) {
1092
- return { content: [{ type: "text", text: "sessionId is required for show_session" }], details: {}, isError: true };
1135
+ return createBoardErrorResult("sessionId is required for show_session", "missing-session-id", "warning");
1093
1136
  }
1094
1137
  assertSafeSessionId(params.sessionId);
1095
1138
  const paths = getSessionPaths(ctx.cwd, params.sessionId);
@@ -1108,13 +1151,18 @@ ${stateJson}`
1108
1151
 
1109
1152
  if (params.action === "update_task") {
1110
1153
  if (!params.sessionId || !params.taskId) {
1111
- return { content: [{ type: "text", text: "sessionId and taskId are required for update_task" }], details: {}, isError: true };
1154
+ return createBoardErrorResult("sessionId and taskId are required for update_task", "missing-task-context", "warning");
1112
1155
  }
1113
1156
  assertSafeTaskId(params.taskId);
1114
1157
  const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
1115
1158
  const idx = sessionState.tasks.findIndex((task) => task.id === params.taskId);
1116
1159
  if (idx === -1) {
1117
- return { content: [{ type: "text", text: `Task ${params.taskId} not found in session ${params.sessionId}` }], details: {}, isError: true };
1160
+ return createBoardErrorResult(
1161
+ `Task ${params.taskId} not found in session ${params.sessionId}`,
1162
+ "task-not-found",
1163
+ "error",
1164
+ { sessionId: params.sessionId, taskId: params.taskId },
1165
+ );
1118
1166
  }
1119
1167
  const current = sessionState.tasks[idx];
1120
1168
  const checklist = resolveChecklistState(current.checklist, params.checklist, params.checklistUpdates);
@@ -1127,16 +1175,12 @@ ${stateJson}`
1127
1175
  if (params.status === "completed") {
1128
1176
  const completionGateError = getCompletionGateError(nextTask);
1129
1177
  if (completionGateError) {
1130
- return {
1131
- content: [{ type: "text", text: completionGateError }],
1132
- details: {
1133
- sessionId: params.sessionId,
1134
- taskId: current.id,
1135
- incompleteChecklistItems: getIncompleteChecklistItems(nextTask.checklist),
1136
- checklist: nextTask.checklist,
1137
- },
1138
- isError: true,
1139
- };
1178
+ return createBoardErrorResult(completionGateError, "completion-gate", "warning", {
1179
+ sessionId: params.sessionId,
1180
+ taskId: current.id,
1181
+ incompleteChecklistItems: getIncompleteChecklistItems(nextTask.checklist),
1182
+ checklist: nextTask.checklist,
1183
+ });
1140
1184
  }
1141
1185
  }
1142
1186
  sessionState.tasks[idx] = nextTask;
@@ -1174,7 +1218,11 @@ ${stateJson}`
1174
1218
 
1175
1219
  if (params.action === "expand_stage") {
1176
1220
  if (!params.sessionId || !params.stage || !params.tasks?.length) {
1177
- return { content: [{ type: "text", text: "sessionId, stage, and at least one task are required for expand_stage" }], details: {}, isError: true };
1221
+ return createBoardErrorResult(
1222
+ "sessionId, stage, and at least one task are required for expand_stage",
1223
+ "invalid-expansion",
1224
+ "warning",
1225
+ );
1178
1226
  }
1179
1227
 
1180
1228
  const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
@@ -1257,6 +1305,8 @@ ${stateJson}`
1257
1305
  details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode },
1258
1306
  };
1259
1307
  },
1308
+ renderCall: renderTakomiBoardCall,
1309
+ renderResult: (result, options, theme, context) => renderTakomiBoardResult(result, options, theme, context?.args),
1260
1310
  });
1261
1311
 
1262
1312
  pi.on("input", async (event) => {
@@ -1271,20 +1321,39 @@ ${stateJson}`
1271
1321
  state.enabled = true;
1272
1322
  try {
1273
1323
  const cwd = runtimeCtx?.cwd ?? process.cwd();
1274
- const preview = previewTakomiRoutingPolicy(cwd, text, { scope: "global" });
1324
+ const availableModels = (() => {
1325
+ try {
1326
+ const available = (runtimeCtx as typeof runtimeCtx & { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } })?.modelRegistry?.getAvailable?.() ?? [];
1327
+ return available.map((model) => `${model.provider ? `${model.provider}/` : ""}${model.id ?? model.name ?? ""}`).filter(Boolean);
1328
+ } catch {
1329
+ return [];
1330
+ }
1331
+ })();
1332
+ const preview = previewTakomiRoutingPolicy(cwd, text, { scope: "global", availableModels });
1333
+ const activePolicy = await resolveTakomiRoutingPolicy(cwd);
1334
+ const replacementWarning = activePolicy.text && activePolicy.text.trim().length > preview.policy.trim().length * 2
1335
+ ? `WARNING: This input is much shorter than the active policy (${preview.policy.length} vs ${activePolicy.text.length} characters). It will replace the file, not merge into it. Inspect any referenced full source before applying.`
1336
+ : "The supplied text replaces the policy file exactly; it is not merged with the current policy.";
1275
1337
  return { action: "transform", text: [
1276
1338
  "Review this Takomi routing policy extraction before it is saved.",
1277
1339
  "",
1278
1340
  "Rules:",
1279
1341
  "- Do not invent providers or model IDs not grounded in the policy.",
1280
- "- Providerless names like GPT-5.5 are routing intent unless a preferred provider/router is declared.",
1281
- "- Valid Takomi roles are: general, orchestrator, architect, designer, coder, reviewer.",
1282
- "- If the extraction is correct and safe, call takomi_apply_routing_policy with scope=global and the exact original policy text.",
1283
- "- If it is ambiguous or wrong, explain what the user should clarify and do not call the tool.",
1342
+ "- Providerless names are valid routing intent. Require a provider only for executable model overrides.",
1343
+ "- Check the available Pi model registry below before asking whether a named model exists or what its exact ID is.",
1344
+ "- Conditional task routes need not be forced into role-wide agentOverrides.",
1345
+ "- Valid role-wide Takomi overrides are: general, orchestrator, architect, designer, coder, reviewer. Other headings may be policy concepts or execution routes.",
1346
+ "- Preserve the user's complete authored policy. If this is a summary/excerpt and a referenced full source exists, inspect and apply that source rather than overwriting it with the excerpt.",
1347
+ "- If correct and safe, call takomi_apply_routing_policy with scope=global and the complete intended policy text.",
1348
+ "- Ask only for unresolved provider/account choices or genuine ambiguity; do not ask for facts available in registry or files.",
1284
1349
  "",
1285
1350
  "Deterministic extraction:",
1286
1351
  renderRoutingPolicyPreview(preview),
1287
1352
  "",
1353
+ replacementWarning,
1354
+ "",
1355
+ availableModels.length ? `Available Pi models:\n${availableModels.map((model) => `- ${model}`).join("\n")}` : "Available Pi models: registry unavailable; inspect it before asking the user if possible.",
1356
+ "",
1288
1357
  "Original policy text:",
1289
1358
  "```",
1290
1359
  preview.policy,
@@ -205,7 +205,7 @@ export async function resolveTakomiRoutingPolicy(cwd: string): Promise<ResolvedR
205
205
  return { source: "missing" };
206
206
  }
207
207
 
208
- export function previewTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): RoutingPolicyPreviewResult {
208
+ export function previewTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope; availableModels?: string[] } = {}): RoutingPolicyPreviewResult {
209
209
  const policy = extractQuotedPolicy(input);
210
210
  if (!policy) throw new Error("No routing policy text found. Paste the policy after /takomi routing or inside triple quotes.");
211
211
 
@@ -217,7 +217,19 @@ export function previewTakomiRoutingPolicy(cwd: string, input: string, options:
217
217
  ? path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE)
218
218
  : GLOBAL_PI_SETTINGS_PATH;
219
219
  const { overrides, detected } = deriveSubagentDefaults(policy);
220
- return { scope, policy, policyPath, settingsPath, detectedDefaults: detected, overrides };
220
+
221
+ // Resolve named model families from Pi's registry for review visibility. These
222
+ // are conditional routing intents, not role-wide defaults, so do not invent
223
+ // agentOverrides merely to make the extraction non-empty.
224
+ const availableModels = [...new Set(options.availableModels ?? [])];
225
+ for (const alias of ["luna", "sol", "terra"]) {
226
+ if (!new RegExp(`\\b${alias}\\b`, "i").test(policy)) continue;
227
+ const matches = availableModels.filter((model) => new RegExp(`(?:^|/)gpt[-_.]?5\\.6[-_.]?${alias}$`, "i").test(model));
228
+ if (matches.length === 1) detected.push(`${alias[0].toUpperCase()}${alias.slice(1)} intent resolves to ${matches[0]} (conditional route)`);
229
+ else if (matches.length > 1) detected.push(`${alias[0].toUpperCase()}${alias.slice(1)} intent matches available models: ${matches.join(", ")}`);
230
+ else detected.push(`${alias[0].toUpperCase()}${alias.slice(1)} remains a providerless conditional routing intent`);
231
+ }
232
+ return { scope, policy, policyPath, settingsPath, detectedDefaults: [...new Set(detected)], overrides };
221
233
  }
222
234
 
223
235
  export async function installTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): Promise<RoutingPolicyInstallResult> {