takomi 2.1.44 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.pi/README.md +9 -8
  2. package/.pi/agents/architect.md +2 -2
  3. package/.pi/agents/designer.md +2 -2
  4. package/.pi/agents/worker.md +32 -0
  5. package/.pi/extensions/oauth-router/commands.ts +66 -35
  6. package/.pi/extensions/oauth-router/index.ts +51 -7
  7. package/.pi/extensions/oauth-router/report-ui.ts +205 -0
  8. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
  9. package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
  10. package/.pi/extensions/takomi-context-manager/index.ts +3 -2
  11. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +50 -117
  12. package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
  13. package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
  14. package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
  15. package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
  16. package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
  17. package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
  18. package/.pi/extensions/takomi-context-manager/types.ts +6 -0
  19. package/.pi/extensions/takomi-runtime/command-text.ts +5 -4
  20. package/.pi/extensions/takomi-runtime/commands.ts +58 -25
  21. package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
  22. package/.pi/extensions/takomi-runtime/index.ts +385 -124
  23. package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +262 -326
  24. package/.pi/extensions/takomi-runtime/profile.ts +9 -8
  25. package/.pi/extensions/takomi-runtime/routing-policy.ts +97 -66
  26. package/.pi/extensions/takomi-runtime/shared.ts +7 -30
  27. package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
  28. package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
  29. package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
  30. package/.pi/extensions/takomi-subagents/agents.ts +9 -0
  31. package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
  32. package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
  33. package/.pi/extensions/takomi-subagents/index.ts +50 -1
  34. package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
  35. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +62 -47
  36. package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
  37. package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
  38. package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
  39. package/.pi/extensions/takomi-subagents/tool-runner.ts +198 -29
  40. package/.pi/settings.json +39 -36
  41. package/.pi/takomi/model-routing.md +288 -3
  42. package/.pi/takomi-profile.json +54 -50
  43. package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
  44. package/package.json +4 -3
  45. package/src/pi-takomi-core/orchestration.ts +39 -21
  46. package/src/pi-takomi-core/routing.ts +8 -8
  47. package/src/pi-takomi-core/types.ts +35 -5
  48. package/src/pi-takomi-core/workflows.ts +86 -45
  49. package/src/skills-catalog.js +2 -202
  50. package/src/skills-installer.js +4 -1
@@ -1,4 +1,5 @@
1
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { createHash, randomUUID } from "node:crypto";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
@@ -6,6 +7,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
6
7
  import { Type } from "typebox";
7
8
  import {
8
9
  buildSessionState,
10
+ canonicalizeTakomiPersona,
9
11
  createSessionId,
10
12
  createLifecycleStarterSession,
11
13
  createTask,
@@ -13,7 +15,6 @@ import {
13
15
  getSessionPaths,
14
16
  getNextTaskId,
15
17
  getWorkflowDefinition,
16
- listWorkflowDefinitions,
17
18
  markStageExpanded,
18
19
  normalizeSessionState,
19
20
  renderMasterPlan,
@@ -28,6 +29,7 @@ import {
28
29
  type TakomiDispatchPolicy,
29
30
  type TakomiLaunchMode,
30
31
  type TakomiProfile,
32
+ type TakomiPersona,
31
33
  type TakomiRole,
32
34
  type TakomiThinkingLevel,
33
35
  type TakomiWorkflowId,
@@ -50,12 +52,35 @@ import {
50
52
  } from "./shared";
51
53
  import { TakomiContextPanel, wireContextPanel } from "./context-panel";
52
54
  import { registerTakomiCommands } from "./commands";
55
+ import { USER_GATE_AUTO_PROVENANCE_ENTRY } from "./gate-provenance";
53
56
  import {
54
57
  DEFAULT_TAKOMI_PROFILE,
55
58
  getProfileDefaults,
56
59
  loadTakomiProfile,
57
60
  } from "./profile";
58
- import { installTakomiRoutingPolicy, previewTakomiRoutingPolicy, renderRoutingPolicyPreview, resolveTakomiRoutingPolicy } from "./routing-policy";
61
+ import {
62
+ installTakomiRoutingConfig,
63
+ installTakomiRoutingPolicy,
64
+ previewTakomiRoutingConfig,
65
+ previewTakomiRoutingPolicy,
66
+ renderRoutingPolicyPreview,
67
+ renderTakomiRoutingConfigPreview,
68
+ resolveTakomiRoutingPolicy,
69
+ } from "./routing-policy";
70
+ import {
71
+ discoverWorkflowPlaybooks,
72
+ showWorkflowCatalogForBoard,
73
+ } from "./workflow-catalog";
74
+ import {
75
+ renderTakomiBoardCall,
76
+ renderTakomiBoardResult,
77
+ renderTakomiModeCall,
78
+ renderTakomiModeResult,
79
+ renderTakomiRoutingCall,
80
+ renderTakomiRoutingResult,
81
+ renderTakomiWorkflowCall,
82
+ renderTakomiWorkflowResult,
83
+ } from "./tool-renderers";
59
84
 
60
85
  type TakomiModeSource = "idle" | "manual" | "model" | "board";
61
86
 
@@ -168,7 +193,7 @@ const ThinkingSchema = Type.Union([
168
193
  Type.Literal("xhigh"),
169
194
  ]);
170
195
 
171
- const TakomiModeSchema = StringEnum(["idle", "code", "orchestrate", "review", "genesis", "design", "build"] as const);
196
+ const TakomiModeSchema = StringEnum(["idle", "code", "review", "orchestrate"] as const);
172
197
 
173
198
  function cloneState(state: TakomiState): TakomiState {
174
199
  return { ...state };
@@ -190,12 +215,9 @@ function formatState(state: TakomiState): string {
190
215
  ].filter(Boolean).join(" | ");
191
216
  }
192
217
 
193
- function setStageAndWorkflow(state: TakomiState, stage: VibeLifecycleStage, options?: { preserveRole?: boolean }) {
218
+ function setStageAndWorkflow(state: TakomiState, stage: VibeLifecycleStage, _options?: { preserveRole?: boolean }) {
194
219
  state.stage = stage;
195
220
  state.workflow = stage === "genesis" ? "vibe-genesis" : stage === "design" ? "vibe-design" : "vibe-build";
196
- if (!options?.preserveRole) {
197
- state.role = stage === "design" ? "design" : stage === "build" ? "orchestrator" : "architect";
198
- }
199
221
  state.enabled = true;
200
222
  }
201
223
 
@@ -212,17 +234,22 @@ function fallbackRolePrompt(role: TakomiRole): string {
212
234
  "You are operating in Takomi architect mode.",
213
235
  "Clarify scope, define acceptance criteria, and build the project foundation before design or implementation.",
214
236
  ].join("\n");
215
- case "design":
237
+ case "designer":
216
238
  return [
217
- "You are operating in Takomi design mode.",
218
- "Translate genesis context into build-ready UX and visual direction.",
239
+ "You are operating as the Takomi UI/UX designer.",
240
+ "Translate genesis context into build-ready UI, UX, mockups, and visual direction; never use Design for application architecture.",
219
241
  ].join("\n");
220
- case "code":
242
+ case "coder":
221
243
  return [
222
244
  "You are operating in Takomi code mode.",
223
245
  "Implement directly, keep scope controlled, and verify after changes.",
224
246
  ].join("\n");
225
- case "review":
247
+ case "worker":
248
+ return [
249
+ "You are operating as the Takomi worker.",
250
+ "Execute clear non-coding repository tasks and produce requested artifacts without inventing product or architecture decisions.",
251
+ ].join("\n");
252
+ case "reviewer":
226
253
  return [
227
254
  "You are operating in Takomi review mode.",
228
255
  "Focus on correctness, risk, omissions, and actionable review feedback.",
@@ -239,9 +266,10 @@ function agentFileNameForRole(role: TakomiRole): string | undefined {
239
266
  switch (role) {
240
267
  case "orchestrator": return "orchestrator.md";
241
268
  case "architect": return "architect.md";
242
- case "design": return "designer.md";
243
- case "code": return "coder.md";
244
- case "review": return "reviewer.md";
269
+ case "designer": return "designer.md";
270
+ case "coder": return "coder.md";
271
+ case "worker": return "worker.md";
272
+ case "reviewer": return "reviewer.md";
245
273
  default: return undefined;
246
274
  }
247
275
  }
@@ -399,6 +427,24 @@ function getIncompleteChecklistItems(checklist?: OrchestratorTask["checklist"]):
399
427
  .map((item) => item.text);
400
428
  }
401
429
 
430
+ type BoardErrorSeverity = "warning" | "error";
431
+
432
+ function createBoardErrorResult(
433
+ text: string,
434
+ code: string,
435
+ severity: BoardErrorSeverity,
436
+ details: Record<string, unknown> = {},
437
+ ) {
438
+ return {
439
+ content: [{ type: "text" as const, text }],
440
+ // Keep this semantic error independent of Pi's transport-level isError.
441
+ // Renderers use it to retain warning/error meaning even when Pi does not
442
+ // pass top-level result flags back into renderResult.
443
+ details: { ...details, error: { code, message: text, severity } },
444
+ isError: true,
445
+ };
446
+ }
447
+
402
448
  function getCompletionGateError(task: Pick<OrchestratorTask, "id" | "title" | "checklist">): string | undefined {
403
449
  if (!task.checklist?.length) {
404
450
  return `Task ${task.id} cannot be marked completed until it has a checklist.`;
@@ -545,8 +591,33 @@ async function writeTaskArtifact(paths: ReturnType<typeof getSessionPaths>, stat
545
591
  if (existingPath !== targetPath) await rm(existingPath, { force: true });
546
592
  }
547
593
 
548
- async function syncTaskArtifacts(cwd: string, session: OrchestratorSessionState) {
549
- const normalizedState = normalizeSessionState(session);
594
+ type MasterPlanDisposition = "written" | "preserved" | "unchanged" | "generated";
595
+
596
+ function sha256(content: string): string {
597
+ return createHash("sha256").update(content).digest("hex");
598
+ }
599
+
600
+ async function atomicWrite(filePath: string, content: string): Promise<void> {
601
+ const temporary = `${filePath}.${randomUUID()}.tmp`;
602
+ await writeFile(temporary, content, "utf8");
603
+ await rename(temporary, filePath);
604
+ }
605
+
606
+ async function atomicReplaceExpected(filePath: string, content: string, expectedSha256: string): Promise<{ ok: true } | { ok: false; currentSha256: string }> {
607
+ const temporary = `${filePath}.${randomUUID()}.tmp`;
608
+ await writeFile(temporary, content, "utf8");
609
+ const current = await readFile(filePath, "utf8");
610
+ const currentSha256 = sha256(current);
611
+ if (currentSha256 !== expectedSha256) {
612
+ await rm(temporary, { force: true });
613
+ return { ok: false, currentSha256 };
614
+ }
615
+ await rename(temporary, filePath);
616
+ return { ok: true };
617
+ }
618
+
619
+ async function syncTaskArtifacts(cwd: string, session: OrchestratorSessionState, incomingMasterPlan?: string) {
620
+ let normalizedState = normalizeSessionState(session);
550
621
  assertSafeSessionId(normalizedState.sessionId);
551
622
  assertSafeTasks(normalizedState.tasks);
552
623
  const paths = getSessionPaths(cwd, normalizedState.sessionId);
@@ -555,10 +626,51 @@ async function syncTaskArtifacts(cwd: string, session: OrchestratorSessionState)
555
626
  await mkdir(paths.completed, { recursive: true });
556
627
  await mkdir(paths.blocked, { recursive: true });
557
628
  await mkdir(paths.stateDir, { recursive: true });
558
- const existingMasterPlan = await readFile(paths.masterPlan, "utf8").catch(() => "");
559
- if (!existingMasterPlan || existingMasterPlan.includes("takomi-generated-master-plan")) {
560
- await writeFile(paths.masterPlan, renderMasterPlan(normalizedState), "utf8");
629
+
630
+ const existing = await readFile(paths.masterPlan, "utf8").catch(() => undefined);
631
+ const existingHash = existing === undefined ? undefined : sha256(existing);
632
+ const recorded = normalizedState.artifacts?.masterPlan;
633
+ const recordedStillMatches = Boolean(recorded && existingHash === recorded.sha256);
634
+ const existingOwner = recordedStillMatches
635
+ ? recorded!.owner
636
+ : existing?.includes("takomi-generated-master-plan")
637
+ ? "board" as const
638
+ : "human" as const;
639
+ let finalContent = existing;
640
+ let owner = existingOwner;
641
+ let masterPlanDisposition: MasterPlanDisposition;
642
+ let masterPlanWarning: string | undefined;
643
+
644
+ if (existing === undefined) {
645
+ if (incomingMasterPlan !== undefined) {
646
+ finalContent = incomingMasterPlan;
647
+ owner = "caller";
648
+ masterPlanDisposition = "written";
649
+ } else {
650
+ finalContent = renderMasterPlan(normalizedState);
651
+ owner = "board";
652
+ masterPlanDisposition = "generated";
653
+ }
654
+ await atomicWrite(paths.masterPlan, finalContent);
655
+ } else if (incomingMasterPlan === undefined) {
656
+ masterPlanDisposition = existingOwner === "board" ? "unchanged" : "preserved";
657
+ } else if (incomingMasterPlan === existing) {
658
+ masterPlanDisposition = "unchanged";
659
+ } else if (existingOwner === "board") {
660
+ finalContent = incomingMasterPlan;
661
+ owner = "caller";
662
+ masterPlanDisposition = "written";
663
+ await atomicWrite(paths.masterPlan, finalContent);
664
+ } else {
665
+ masterPlanDisposition = "preserved";
666
+ masterPlanWarning = "Preserved the existing human/caller-authored master_plan.md because incoming content differed. Use replace_master_plan with confirmation and the current SHA-256 for an intentional replacement.";
561
667
  }
668
+
669
+ const finalHash = sha256(finalContent ?? "");
670
+ normalizedState = normalizeSessionState({
671
+ ...normalizedState,
672
+ artifacts: { masterPlan: { owner, sha256: finalHash, lastSeenAt: new Date().toISOString() } },
673
+ });
562
674
  const validation = validateSessionState(normalizedState);
563
675
  await writeFile(paths.summary, [
564
676
  `# Orchestrator Summary: ${normalizedState.title}`,
@@ -568,6 +680,7 @@ async function syncTaskArtifacts(cwd: string, session: OrchestratorSessionState)
568
680
  `- Machine state: ${paths.stateFile}`,
569
681
  `- Runtime mode: ${normalizedState.mode}`,
570
682
  `- Session intent: ${normalizedState.sessionIntent ?? "full-project"}`,
683
+ `- Master plan: ${masterPlanDisposition} (${owner}, sha256 ${finalHash})`,
571
684
  `- Validation: ${validation.ok ? "PASS" : "ERRORS"} (${validation.errors.length} errors, ${validation.warnings.length} warnings)`,
572
685
  "",
573
686
  "## Validation",
@@ -576,15 +689,13 @@ async function syncTaskArtifacts(cwd: string, session: OrchestratorSessionState)
576
689
  ].join("\n"), "utf8");
577
690
  await writeFile(paths.stateFile, serializeSessionState(normalizedState), "utf8");
578
691
 
579
- for (const task of normalizedState.tasks) {
580
- await writeTaskArtifact(paths, normalizedState, task);
581
- }
692
+ for (const task of normalizedState.tasks) await writeTaskArtifact(paths, normalizedState, task);
582
693
 
583
- return paths;
694
+ return { ...paths, masterPlanDisposition, masterPlanWarning, masterPlanSha256: finalHash };
584
695
  }
585
696
 
586
- async function writeOrchestratorSession(cwd: string, session: OrchestratorSessionState) {
587
- return syncTaskArtifacts(cwd, session);
697
+ async function writeOrchestratorSession(cwd: string, session: OrchestratorSessionState, incomingMasterPlan?: string) {
698
+ return syncTaskArtifacts(cwd, session, incomingMasterPlan);
588
699
  }
589
700
 
590
701
  type IncomingTask = {
@@ -592,12 +703,13 @@ type IncomingTask = {
592
703
  title: string;
593
704
  taskMarkdown?: string;
594
705
  status?: OrchestratorTaskStatus;
595
- role: TakomiRole;
706
+ role: TakomiPersona | "general" | "design" | "code" | "review";
596
707
  stage?: VibeLifecycleStage;
597
708
  workflow?: TakomiWorkflowId;
598
709
  parentTaskId?: string;
599
710
  preferredAgent?: string;
600
711
  preferredModel?: string;
712
+ preferredModelConfirmed?: boolean;
601
713
  preferredModelHint?: string;
602
714
  preferredThinking?: TakomiThinkingLevel;
603
715
  fallbackModels?: string[];
@@ -609,12 +721,21 @@ type IncomingTask = {
609
721
  scope?: string[];
610
722
  definitionOfDone?: string[];
611
723
  expectedArtifacts?: string[];
724
+ requiredCapabilities?: string[];
612
725
  dependencies?: string[];
613
726
  reviewCheckpoint?: string;
614
727
  instructions?: string[];
615
728
  conversationId?: string;
616
729
  };
617
730
 
731
+ const CANONICAL_TAKOMI_PERSONAS = new Set(["architect", "designer", "coder", "worker", "reviewer", "orchestrator"]);
732
+ const WRITE_CAPABLE_PERSONAS = new Set(["architect", "designer", "coder", "worker"]);
733
+
734
+ function incomingTaskRequiresWrite(task: IncomingTask): boolean {
735
+ if (task.requiredCapabilities?.some((capability) => /^(write|edit|write-docs|write-code)$/i.test(capability))) return true;
736
+ return (task.expectedArtifacts ?? []).some((artifact) => /(?:\.md\b|\bmarkdown\b|\bfile\b|\bdocument\b)/i.test(artifact));
737
+ }
738
+
618
739
  async function materializeTasksFromInput(
619
740
  ctx: ExtensionContext,
620
741
  currentTasks: OrchestratorTask[],
@@ -626,20 +747,33 @@ async function materializeTasksFromInput(
626
747
  for (const task of incoming) {
627
748
  if (task.id) assertSafeTaskId(task.id);
628
749
  const stage = task.stage ?? stageOverride;
629
- const defaults = getProfileDefaults(activeProfile, task.role, stage);
630
- const fallbackModels = [
631
- ...(task.fallbackModels ?? []),
632
- ...(defaults.fallbackModels ?? []),
633
- ];
750
+ const persona = canonicalizeTakomiPersona(task.role);
751
+ if (task.preferredModel && task.preferredModelConfirmed !== true) {
752
+ throw new Error(`Task '${task.title}' specifies preferredModel '${task.preferredModel}' without preferredModelConfirmed=true.`);
753
+ }
754
+ const defaults = getProfileDefaults(activeProfile, persona, stage);
755
+ const selectedAgent = task.preferredAgent ?? defaults.agent ?? persona;
756
+ if (!CANONICAL_TAKOMI_PERSONAS.has(selectedAgent)) {
757
+ throw new Error(`Task '${task.title}' requested hidden or unknown agent '${selectedAgent}'. Use architect, designer, coder, worker, reviewer, or orchestrator.`);
758
+ }
759
+ if (incomingTaskRequiresWrite(task) && !WRITE_CAPABLE_PERSONAS.has(selectedAgent)) {
760
+ throw new Error(`Task '${task.title}' requires writable artifacts, but '${selectedAgent}' is inspection/coordination-only.`);
761
+ }
762
+ const fallbackModels = task.fallbackModels !== undefined
763
+ ? task.fallbackModels
764
+ : task.preferredModel
765
+ ? []
766
+ : defaults.fallbackModels ?? [];
634
767
  const requestedModel = task.preferredModel ?? defaults.model;
635
768
  const resolvedModel = await resolvePreferredModel(ctx, requestedModel, fallbackModels);
636
769
  const id = task.id ?? getNextTaskId(nextTasks);
637
- nextTasks.push(createTask(id, task.title, task.role, {
770
+ nextTasks.push(createTask(id, task.title, persona, {
638
771
  stage,
639
772
  workflow: task.workflow,
640
773
  parentTaskId: task.parentTaskId,
641
- preferredAgent: task.preferredAgent ?? defaults.agent,
774
+ preferredAgent: selectedAgent,
642
775
  preferredModel: resolvedModel.model,
776
+ preferredModelConfirmed: task.preferredModel ? true : undefined,
643
777
  preferredModelHint: [task.preferredModelHint, resolvedModel.warning].filter(Boolean).join(" ").trim() || undefined,
644
778
  preferredThinking: task.preferredThinking ?? defaults.thinking,
645
779
  fallbackModels: fallbackModels.length ? fallbackModels : undefined,
@@ -651,6 +785,7 @@ async function materializeTasksFromInput(
651
785
  scope: task.scope,
652
786
  definitionOfDone: task.definitionOfDone,
653
787
  expectedArtifacts: task.expectedArtifacts,
788
+ requiredCapabilities: task.requiredCapabilities,
654
789
  dependencies: task.dependencies,
655
790
  reviewCheckpoint: task.reviewCheckpoint,
656
791
  instructions: task.instructions,
@@ -665,10 +800,11 @@ async function applyProfileDefaultsToTasks(ctx: ExtensionContext, tasks: Orchest
665
800
  const nextTasks: OrchestratorTask[] = [];
666
801
  for (const task of tasks) {
667
802
  const defaults = getProfileDefaults(activeProfile, task.role, task.stage);
668
- const fallbackModels = [
669
- ...(task.fallbackModels ?? []),
670
- ...(defaults.fallbackModels ?? []),
671
- ];
803
+ const fallbackModels = task.fallbackModels !== undefined
804
+ ? task.fallbackModels
805
+ : task.preferredModel
806
+ ? []
807
+ : defaults.fallbackModels ?? [];
672
808
  const requestedModel = task.preferredModel ?? defaults.model;
673
809
  const resolvedModel = await resolvePreferredModel(ctx, requestedModel, fallbackModels);
674
810
  nextTasks.push({
@@ -688,6 +824,10 @@ function installTakomiFooter(ctx: ExtensionContext, stateRef: { current: TakomiS
688
824
  ctx.ui.setFooter((tui, theme, footerData) => new TakomiFooterComponent(tui, theme, footerData, ctx, () => stateRef.current));
689
825
  }
690
826
 
827
+ function hasVisibleRuntimeWidget(state: TakomiState): boolean {
828
+ return state.enabled && (state.modeSource ?? "idle") !== "idle";
829
+ }
830
+
691
831
  async function refreshUi(
692
832
  ctx: ExtensionContext,
693
833
  state: TakomiState,
@@ -742,6 +882,12 @@ export default function takomiRuntime(pi: ExtensionAPI) {
742
882
  pi.appendEntry(STATE_ENTRY, state);
743
883
  }
744
884
 
885
+ // This is intentionally separate from generic runtime-state persistence.
886
+ // Only user slash-command handlers receive the recorder below.
887
+ function recordUserGateAutoProvenance(authorized: boolean): void {
888
+ pi.appendEntry(USER_GATE_AUTO_PROVENANCE_ENTRY, { authorized });
889
+ }
890
+
745
891
  function syncContextPanelState() {
746
892
  contextPanel.setRuntimeState({
747
893
  role: state.role,
@@ -829,6 +975,7 @@ export default function takomiRuntime(pi: ExtensionAPI) {
829
975
  registerTakomiCommands(pi, {
830
976
  getState: () => state,
831
977
  updateState,
978
+ recordUserGateAutoProvenance,
832
979
  setStageAndWorkflow: (stage, options) => setStageAndWorkflow(state, stage, options),
833
980
  hasGenesisArtifacts,
834
981
  subagentController,
@@ -857,12 +1004,13 @@ export default function takomiRuntime(pi: ExtensionAPI) {
857
1004
  },
858
1005
  resetRuntime: async (ctx) => {
859
1006
  await updateState(ctx, () => {
1007
+ recordUserGateAutoProvenance(false);
860
1008
  state = cloneState(DEFAULT_STATE);
861
1009
  activeSubagentLabel = undefined;
862
1010
  activeSubagentAgent = undefined;
863
1011
  activeSubagentTask = undefined;
864
1012
  activeSubagentStatus = undefined;
865
- }, "Takomi runtime state reset");
1013
+ }, () => hasVisibleRuntimeWidget(state) ? "" : "Takomi runtime state reset");
866
1014
  subagentController.reset(ctx);
867
1015
  contextPanel.resetSession();
868
1016
  contextPanel.show(ctx);
@@ -871,7 +1019,6 @@ export default function takomiRuntime(pi: ExtensionAPI) {
871
1019
 
872
1020
 
873
1021
  async function applyTakomiMode(ctx: ExtensionContext, mode: string, source: TakomiModeSource, reason?: string): Promise<string> {
874
- const hasGenesis = await hasGenesisArtifacts(ctx.cwd);
875
1022
  state.enabled = true;
876
1023
  state.modeSource = source;
877
1024
  state.modeReason = reason?.trim() || undefined;
@@ -889,30 +1036,17 @@ export default function takomiRuntime(pi: ExtensionAPI) {
889
1036
  case "code":
890
1037
  state.autoOrch = false;
891
1038
  state.planMode = false;
892
- state.role = "code";
893
- state.stage = undefined;
894
- state.workflow = undefined;
1039
+ state.role = "coder";
895
1040
  break;
896
1041
  case "review":
897
1042
  state.autoOrch = false;
898
1043
  state.planMode = true;
899
- state.role = "review";
900
- state.stage = undefined;
901
- state.workflow = undefined;
1044
+ state.role = "reviewer";
902
1045
  break;
903
1046
  case "orchestrate":
904
1047
  state.autoOrch = true;
905
1048
  state.planMode = true;
906
1049
  state.role = "orchestrator";
907
- state.stage = hasGenesis ? "build" : "genesis";
908
- state.workflow = hasGenesis ? "vibe-build" : "vibe-genesis";
909
- break;
910
- case "genesis":
911
- case "design":
912
- case "build":
913
- setStageAndWorkflow(state, mode);
914
- state.autoOrch = mode === "build";
915
- state.planMode = mode !== "build";
916
1050
  break;
917
1051
  }
918
1052
 
@@ -920,14 +1054,16 @@ export default function takomiRuntime(pi: ExtensionAPI) {
920
1054
  syncContextPanelState();
921
1055
  await refreshUi(ctx, state, footerStateRef);
922
1056
  const label = state.modeSource === "idle" ? "idle" : `${state.modeSource}:${state.stage ?? state.role}`;
923
- return `Takomi mode set to ${label}${state.modeReason ? ` (${state.modeReason})` : ""}.`;
1057
+ const text = `Takomi mode set to ${label}${state.modeReason ? ` (${state.modeReason})` : ""}.`;
1058
+ if (!hasVisibleRuntimeWidget(state)) ctx.ui.notify(text, "info");
1059
+ return text;
924
1060
  }
925
1061
 
926
1062
  pi.registerTool({
927
1063
  name: "takomi_mode",
928
1064
  label: "Takomi Mode",
929
- description: "Set or clear the visible Takomi runtime mode after the active model decides a task benefits from code, review, lifecycle, or orchestration handling.",
930
- promptSnippet: "Optional: call takomi_mode when you decide the current request should visibly enter Takomi code/review/orchestration/lifecycle mode. Do not call it for normal creative/general conversation. Use mode=idle to clear Takomi mode.",
1065
+ description: "Set or clear the main-agent Takomi mode. Lifecycle stages are managed separately.",
1066
+ promptSnippet: "Optional: set the main-agent mode to idle, code, review, or orchestrate. Genesis, Design, and Build are lifecycle stages, never modes.",
931
1067
  promptGuidelines: [
932
1068
  "Let the user's request drive the choice; do not switch modes just because a vague word like code/review/build appears alone.",
933
1069
  "Prefer mode=code for direct coding in the current chat, mode=orchestrate only for broad/multi-step durable work, and mode=review for critique/audit/QA.",
@@ -939,22 +1075,23 @@ export default function takomiRuntime(pi: ExtensionAPI) {
939
1075
  }),
940
1076
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
941
1077
  const text = await applyTakomiMode(ctx, params.mode, "model", params.reason);
942
- ctx.ui.notify(text, "info");
943
1078
  return {
944
1079
  content: [{ type: "text", text }],
945
1080
  details: { mode: params.mode, source: state.modeSource, reason: state.modeReason, role: state.role, stage: state.stage, workflow: state.workflow },
946
1081
  };
947
1082
  },
1083
+ renderCall: renderTakomiModeCall,
1084
+ renderResult: (result, options, theme) => renderTakomiModeResult(result, options, theme),
948
1085
  });
949
1086
 
950
1087
  pi.registerTool({
951
1088
  name: "takomi_apply_routing_policy",
952
1089
  label: "Takomi Routing",
953
- description: "Apply a Takomi model-routing policy after deterministic extraction and active-model review.",
954
- promptSnippet: "Save reviewed Takomi model routing policy text to the active global or project policy file.",
1090
+ description: "Save human-readable Takomi model-routing guidance. Markdown remains advisory and is never converted into executable defaults.",
1091
+ promptSnippet: "Save reviewed natural-language routing guidance without changing executable model settings.",
955
1092
  promptGuidelines: [
956
- "Use takomi_apply_routing_policy only after reviewing the deterministic extraction against the original routing policy text.",
957
- "Do not call takomi_apply_routing_policy if the policy is ambiguous, invents providers, or maps to non-Takomi roles.",
1093
+ "Preserve the complete authored guidance.",
1094
+ "Use takomi_config_routing, not Markdown prose, for executable providers, allowlists, and role defaults.",
958
1095
  ],
959
1096
  parameters: Type.Object({
960
1097
  policyText: Type.String({ description: "Original routing policy text to save" }),
@@ -963,7 +1100,15 @@ export default function takomiRuntime(pi: ExtensionAPI) {
963
1100
  }),
964
1101
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
965
1102
  const scope = params.scope ?? "global";
966
- const preview = previewTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
1103
+ const availableModels = (() => {
1104
+ try {
1105
+ const available = (ctx as typeof ctx & { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } }).modelRegistry?.getAvailable?.() ?? [];
1106
+ return available.map((model) => `${model.provider ? `${model.provider}/` : ""}${model.id ?? model.name ?? ""}`).filter(Boolean);
1107
+ } catch {
1108
+ return [];
1109
+ }
1110
+ })();
1111
+ const preview = previewTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope, availableModels });
967
1112
  const result = await installTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
968
1113
  const scopeNote = scope === "global"
969
1114
  ? "This global policy applies unless a project-local override exists."
@@ -986,6 +1131,54 @@ export default function takomiRuntime(pi: ExtensionAPI) {
986
1131
  details: { result, preview, reviewNotes: params.reviewNotes },
987
1132
  };
988
1133
  },
1134
+ renderCall: renderTakomiRoutingCall,
1135
+ renderResult: (result, options, theme) => renderTakomiRoutingResult(result, options, theme),
1136
+ });
1137
+
1138
+ pi.registerTool({
1139
+ name: "takomi_config_routing",
1140
+ label: "Takomi Routing Config",
1141
+ description: "Preview or write structured global/project Takomi routing settings.",
1142
+ promptSnippet: "Configure exact provider-qualified allowlists and persona defaults. Preview first; write only with confirmWrite=true.",
1143
+ parameters: Type.Object({
1144
+ scope: StringEnum(["global", "project"] as const),
1145
+ defaultProvider: Type.Optional(Type.String()),
1146
+ approvedModels: Type.Optional(Type.Array(Type.String())),
1147
+ roleDefaults: Type.Optional(Type.Record(Type.String(), Type.Object({
1148
+ model: Type.Optional(Type.String()),
1149
+ thinking: Type.Optional(ThinkingSchema),
1150
+ fallbackModels: Type.Optional(Type.Array(Type.String())),
1151
+ }))),
1152
+ dryRunAgent: Type.Optional(Type.String()),
1153
+ confirmWrite: Type.Optional(Type.Boolean()),
1154
+ }),
1155
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1156
+ const availableModels = (() => {
1157
+ try {
1158
+ return ((ctx as typeof ctx & { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } }).modelRegistry?.getAvailable?.() ?? [])
1159
+ .map((model) => `${model.provider ? `${model.provider}/` : ""}${model.id ?? model.name ?? ""}`)
1160
+ .filter(Boolean);
1161
+ } catch { return []; }
1162
+ })();
1163
+ const preview = await previewTakomiRoutingConfig(ctx.cwd, params.scope, {
1164
+ defaultProvider: params.defaultProvider,
1165
+ approvedModels: params.approvedModels,
1166
+ roleDefaults: params.roleDefaults,
1167
+ }, availableModels);
1168
+ const dryRun = params.dryRunAgent
1169
+ ? (preview.after.roleDefaults as Record<string, { model?: string }> | undefined)?.[params.dryRunAgent]?.model ?? "Pi/harness default"
1170
+ : undefined;
1171
+ if (params.confirmWrite === true) await installTakomiRoutingConfig(preview);
1172
+ return {
1173
+ content: [{ type: "text", text: [
1174
+ params.confirmWrite === true ? "Takomi routing settings written." : "Takomi routing settings preview only; no files changed.",
1175
+ "",
1176
+ renderTakomiRoutingConfigPreview(preview),
1177
+ dryRun ? `\nDry-run ${params.dryRunAgent}: ${dryRun}` : "",
1178
+ ].filter(Boolean).join("\n") }],
1179
+ details: { preview, written: params.confirmWrite === true, dryRunAgent: params.dryRunAgent, resolvedModel: dryRun },
1180
+ };
1181
+ },
989
1182
  });
990
1183
 
991
1184
  pi.registerTool({
@@ -997,20 +1190,10 @@ export default function takomiRuntime(pi: ExtensionAPI) {
997
1190
  workflow: Type.Optional(StringEnum(["vibe-genesis", "vibe-design", "vibe-build"] as const)),
998
1191
  }),
999
1192
  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
- };
1193
+ return discoverWorkflowPlaybooks(params.workflow);
1013
1194
  },
1195
+ renderCall: renderTakomiWorkflowCall,
1196
+ renderResult: (result, options, theme) => renderTakomiWorkflowResult(result, options, theme),
1014
1197
  });
1015
1198
 
1016
1199
  pi.registerTool({
@@ -1031,7 +1214,7 @@ export default function takomiRuntime(pi: ExtensionAPI) {
1031
1214
  "If a reviewed task needs more work, reuse the task conversationId when you call takomi_subagent again, then update the board with the new result.",
1032
1215
  ],
1033
1216
  parameters: Type.Object({
1034
- action: StringEnum(["init_session", "expand_stage", "show_workflows", "show_session", "update_task"] as const),
1217
+ action: StringEnum(["init_session", "expand_stage", "show_workflows", "show_session", "update_task", "replace_master_plan"] as const),
1035
1218
  title: Type.Optional(Type.String()),
1036
1219
  sessionId: Type.Optional(Type.String()),
1037
1220
  taskId: Type.Optional(Type.String()),
@@ -1048,17 +1231,20 @@ export default function takomiRuntime(pi: ExtensionAPI) {
1048
1231
  done: Type.Optional(Type.Boolean()),
1049
1232
  }))),
1050
1233
  masterPlanMarkdown: Type.Optional(Type.String()),
1234
+ confirmReplaceMasterPlan: Type.Optional(Type.Boolean()),
1235
+ expectedCurrentSha256: Type.Optional(Type.String()),
1051
1236
  tasks: Type.Optional(Type.Array(Type.Object({
1052
1237
  id: Type.Optional(Type.String()),
1053
1238
  title: Type.String(),
1054
1239
  taskMarkdown: Type.Optional(Type.String()),
1055
1240
  status: Type.Optional(StringEnum(["pending", "in-progress", "completed", "blocked"] as const)),
1056
- role: StringEnum(["general", "orchestrator", "architect", "design", "code", "review"] as const),
1241
+ role: StringEnum(["orchestrator", "architect", "designer", "coder", "worker", "reviewer", "general", "design", "code", "review"] as const),
1057
1242
  stage: Type.Optional(StringEnum(["genesis", "design", "build"] as const)),
1058
1243
  workflow: Type.Optional(StringEnum(["vibe-genesis", "vibe-design", "vibe-build"] as const)),
1059
1244
  parentTaskId: Type.Optional(Type.String()),
1060
1245
  preferredAgent: Type.Optional(Type.String()),
1061
1246
  preferredModel: Type.Optional(Type.String()),
1247
+ preferredModelConfirmed: Type.Optional(Type.Boolean()),
1062
1248
  preferredModelHint: Type.Optional(Type.String()),
1063
1249
  preferredThinking: Type.Optional(ThinkingSchema),
1064
1250
  fallbackModels: Type.Optional(Type.Array(Type.String())),
@@ -1072,6 +1258,7 @@ export default function takomiRuntime(pi: ExtensionAPI) {
1072
1258
  scope: Type.Optional(Type.Array(Type.String())),
1073
1259
  definitionOfDone: Type.Optional(Type.Array(Type.String())),
1074
1260
  expectedArtifacts: Type.Optional(Type.Array(Type.String())),
1261
+ requiredCapabilities: Type.Optional(Type.Array(Type.String())),
1075
1262
  dependencies: Type.Optional(Type.Array(Type.String())),
1076
1263
  reviewCheckpoint: Type.Optional(Type.String()),
1077
1264
  instructions: Type.Optional(Type.Array(Type.String())),
@@ -1080,16 +1267,12 @@ export default function takomiRuntime(pi: ExtensionAPI) {
1080
1267
  }),
1081
1268
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1082
1269
  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
- };
1270
+ return showWorkflowCatalogForBoard();
1088
1271
  }
1089
1272
 
1090
1273
  if (params.action === "show_session") {
1091
1274
  if (!params.sessionId) {
1092
- return { content: [{ type: "text", text: "sessionId is required for show_session" }], details: {}, isError: true };
1275
+ return createBoardErrorResult("sessionId is required for show_session", "missing-session-id", "warning");
1093
1276
  }
1094
1277
  assertSafeSessionId(params.sessionId);
1095
1278
  const paths = getSessionPaths(ctx.cwd, params.sessionId);
@@ -1106,15 +1289,60 @@ ${stateJson}`
1106
1289
  };
1107
1290
  }
1108
1291
 
1292
+ if (params.action === "replace_master_plan") {
1293
+ if (!params.sessionId || params.masterPlanMarkdown === undefined || params.confirmReplaceMasterPlan !== true || !params.expectedCurrentSha256) {
1294
+ return createBoardErrorResult(
1295
+ "replace_master_plan requires sessionId, masterPlanMarkdown, confirmReplaceMasterPlan=true, and expectedCurrentSha256",
1296
+ "replacement-confirmation-required",
1297
+ "warning",
1298
+ );
1299
+ }
1300
+ const { state: sessionState, paths } = await loadSessionState(ctx.cwd, params.sessionId);
1301
+ const current = await readFile(paths.masterPlan, "utf8").catch(() => undefined);
1302
+ if (current === undefined) return createBoardErrorResult("Current master_plan.md was not found.", "master-plan-missing", "error");
1303
+ const currentHash = sha256(current);
1304
+ if (currentHash !== params.expectedCurrentSha256) {
1305
+ return createBoardErrorResult(
1306
+ `Master plan SHA-256 mismatch. Expected ${params.expectedCurrentSha256}, current ${currentHash}. No files changed.`,
1307
+ "master-plan-hash-mismatch",
1308
+ "error",
1309
+ { expectedCurrentSha256: params.expectedCurrentSha256, currentSha256: currentHash },
1310
+ );
1311
+ }
1312
+ const replacement = await atomicReplaceExpected(paths.masterPlan, params.masterPlanMarkdown, params.expectedCurrentSha256);
1313
+ if (!replacement.ok) {
1314
+ return createBoardErrorResult(
1315
+ `Master plan changed during replacement. Current SHA-256 is ${replacement.currentSha256}. No replacement was applied.`,
1316
+ "master-plan-hash-mismatch",
1317
+ "error",
1318
+ { expectedCurrentSha256: params.expectedCurrentSha256, currentSha256: replacement.currentSha256 },
1319
+ );
1320
+ }
1321
+ const nextHash = sha256(params.masterPlanMarkdown);
1322
+ const synced = await syncTaskArtifacts(ctx.cwd, {
1323
+ ...sessionState,
1324
+ artifacts: { masterPlan: { owner: "caller", sha256: nextHash, lastSeenAt: new Date().toISOString() } },
1325
+ });
1326
+ return {
1327
+ content: [{ type: "text", text: `Replaced master_plan.md for ${params.sessionId}.\nSHA-256: ${nextHash}` }],
1328
+ details: { sessionId: params.sessionId, paths: synced, masterPlanDisposition: "written", masterPlanSha256: nextHash },
1329
+ };
1330
+ }
1331
+
1109
1332
  if (params.action === "update_task") {
1110
1333
  if (!params.sessionId || !params.taskId) {
1111
- return { content: [{ type: "text", text: "sessionId and taskId are required for update_task" }], details: {}, isError: true };
1334
+ return createBoardErrorResult("sessionId and taskId are required for update_task", "missing-task-context", "warning");
1112
1335
  }
1113
1336
  assertSafeTaskId(params.taskId);
1114
1337
  const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
1115
1338
  const idx = sessionState.tasks.findIndex((task) => task.id === params.taskId);
1116
1339
  if (idx === -1) {
1117
- return { content: [{ type: "text", text: `Task ${params.taskId} not found in session ${params.sessionId}` }], details: {}, isError: true };
1340
+ return createBoardErrorResult(
1341
+ `Task ${params.taskId} not found in session ${params.sessionId}`,
1342
+ "task-not-found",
1343
+ "error",
1344
+ { sessionId: params.sessionId, taskId: params.taskId },
1345
+ );
1118
1346
  }
1119
1347
  const current = sessionState.tasks[idx];
1120
1348
  const checklist = resolveChecklistState(current.checklist, params.checklist, params.checklistUpdates);
@@ -1127,16 +1355,12 @@ ${stateJson}`
1127
1355
  if (params.status === "completed") {
1128
1356
  const completionGateError = getCompletionGateError(nextTask);
1129
1357
  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
- };
1358
+ return createBoardErrorResult(completionGateError, "completion-gate", "warning", {
1359
+ sessionId: params.sessionId,
1360
+ taskId: current.id,
1361
+ incompleteChecklistItems: getIncompleteChecklistItems(nextTask.checklist),
1362
+ checklist: nextTask.checklist,
1363
+ });
1140
1364
  }
1141
1365
  }
1142
1366
  sessionState.tasks[idx] = nextTask;
@@ -1154,6 +1378,7 @@ ${stateJson}`
1154
1378
  {
1155
1379
  sessionIntent: sessionState.sessionIntent,
1156
1380
  lifecycle: sessionState.lifecycle,
1381
+ artifacts: sessionState.artifacts,
1157
1382
  },
1158
1383
  );
1159
1384
  const paths = await syncTaskArtifacts(ctx.cwd, nextState);
@@ -1174,11 +1399,20 @@ ${stateJson}`
1174
1399
 
1175
1400
  if (params.action === "expand_stage") {
1176
1401
  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 };
1402
+ return createBoardErrorResult(
1403
+ "sessionId, stage, and at least one task are required for expand_stage",
1404
+ "invalid-expansion",
1405
+ "warning",
1406
+ );
1178
1407
  }
1179
1408
 
1180
1409
  const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
1181
- const tasks = await materializeTasksFromInput(ctx, sessionState.tasks, params.tasks as IncomingTask[], params.stage);
1410
+ let tasks: OrchestratorTask[];
1411
+ try {
1412
+ tasks = await materializeTasksFromInput(ctx, sessionState.tasks, params.tasks as IncomingTask[], params.stage);
1413
+ } catch (error) {
1414
+ return createBoardErrorResult(error instanceof Error ? error.message : String(error), "invalid-task-routing", "error");
1415
+ }
1182
1416
  let nextState = buildSessionState(
1183
1417
  sessionState.sessionId,
1184
1418
  sessionState.title,
@@ -1187,13 +1421,11 @@ ${stateJson}`
1187
1421
  {
1188
1422
  sessionIntent: sessionState.sessionIntent,
1189
1423
  lifecycle: sessionState.lifecycle,
1424
+ artifacts: sessionState.artifacts,
1190
1425
  },
1191
1426
  );
1192
1427
  nextState = markStageExpanded(nextState, params.stage, params.notes);
1193
- const paths = await writeOrchestratorSession(ctx.cwd, nextState);
1194
- if (params.masterPlanMarkdown?.trim()) {
1195
- await writeFile(paths.masterPlan, params.masterPlanMarkdown.trimEnd() + "\n", "utf8");
1196
- }
1428
+ const paths = await writeOrchestratorSession(ctx.cwd, nextState, params.masterPlanMarkdown);
1197
1429
  for (const task of nextState.tasks) {
1198
1430
  const authored = (params.tasks as IncomingTask[] | undefined)?.find((input) => (input.id ?? task.id) === task.id)?.taskMarkdown;
1199
1431
  if (authored?.trim()) {
@@ -1208,20 +1440,30 @@ ${stateJson}`
1208
1440
  await refreshUi(ctx, state, footerStateRef);
1209
1441
 
1210
1442
  return {
1211
- content: [{ type: "text", text: `Expanded ${params.stage} stage in session ${nextState.sessionId}.\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks)}` }],
1212
- details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode },
1443
+ content: [{ type: "text", text: `Expanded ${params.stage} stage in session ${nextState.sessionId}.\nMaster plan: ${paths.masterPlanDisposition}.${paths.masterPlanWarning ? `\nWARNING: ${paths.masterPlanWarning}` : ""}\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks)}` }],
1444
+ details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode, masterPlanDisposition: paths.masterPlanDisposition },
1213
1445
  };
1214
1446
  }
1215
1447
 
1216
1448
  const sessionId = params.sessionId || createSessionId();
1217
1449
  assertSafeSessionId(sessionId);
1450
+ const existingStatePath = getSessionPaths(ctx.cwd, sessionId).stateFile;
1451
+ const sessionAlreadyExists = await readFile(existingStatePath, "utf8").then(() => true).catch(() => false);
1452
+ if (sessionAlreadyExists) {
1453
+ return createBoardErrorResult(`Session ${sessionId} already exists. Use expand_stage or update_task instead of reinitializing it.`, "session-already-exists", "warning");
1454
+ }
1218
1455
  const title = params.title || "Takomi Session";
1219
1456
  const baseState = params.tasks?.length
1220
1457
  ? buildSessionState(sessionId, title, [], new Date())
1221
1458
  : createLifecycleStarterSession(title, { sessionId });
1222
- const tasks = params.tasks?.length
1223
- ? await materializeTasksFromInput(ctx, baseState.tasks, params.tasks as IncomingTask[], params.stage)
1224
- : await applyProfileDefaultsToTasks(ctx, baseState.tasks);
1459
+ let tasks: OrchestratorTask[];
1460
+ try {
1461
+ tasks = params.tasks?.length
1462
+ ? await materializeTasksFromInput(ctx, baseState.tasks, params.tasks as IncomingTask[], params.stage)
1463
+ : await applyProfileDefaultsToTasks(ctx, baseState.tasks);
1464
+ } catch (error) {
1465
+ return createBoardErrorResult(error instanceof Error ? error.message : String(error), "invalid-task-routing", "error");
1466
+ }
1225
1467
  const nextState = buildSessionState(
1226
1468
  baseState.sessionId,
1227
1469
  baseState.title,
@@ -1232,10 +1474,7 @@ ${stateJson}`
1232
1474
  lifecycle: baseState.lifecycle,
1233
1475
  },
1234
1476
  );
1235
- const paths = await writeOrchestratorSession(ctx.cwd, nextState);
1236
- if (params.masterPlanMarkdown?.trim()) {
1237
- await writeFile(paths.masterPlan, params.masterPlanMarkdown.trimEnd() + "\n", "utf8");
1238
- }
1477
+ const paths = await writeOrchestratorSession(ctx.cwd, nextState, params.masterPlanMarkdown);
1239
1478
  for (const task of nextState.tasks) {
1240
1479
  const authored = (params.tasks as IncomingTask[] | undefined)?.find((input) => (input.id ?? task.id) === task.id)?.taskMarkdown;
1241
1480
  if (authored?.trim()) {
@@ -1253,10 +1492,12 @@ ${stateJson}`
1253
1492
  await refreshUi(ctx, state, footerStateRef);
1254
1493
 
1255
1494
  return {
1256
- content: [{ type: "text", text: `Created Takomi orchestrator session ${nextState.sessionId} in hybrid mode\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks) || "No tasks provided."}` }],
1257
- details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode },
1495
+ content: [{ type: "text", text: `Created Takomi orchestrator session ${nextState.sessionId} in hybrid mode\nMaster plan: ${paths.masterPlanDisposition}.${paths.masterPlanWarning ? `\nWARNING: ${paths.masterPlanWarning}` : ""}\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks) || "No tasks provided."}` }],
1496
+ details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode, masterPlanDisposition: paths.masterPlanDisposition },
1258
1497
  };
1259
1498
  },
1499
+ renderCall: renderTakomiBoardCall,
1500
+ renderResult: (result, options, theme, context) => renderTakomiBoardResult(result, options, theme, context?.args),
1260
1501
  });
1261
1502
 
1262
1503
  pi.on("input", async (event) => {
@@ -1271,20 +1512,38 @@ ${stateJson}`
1271
1512
  state.enabled = true;
1272
1513
  try {
1273
1514
  const cwd = runtimeCtx?.cwd ?? process.cwd();
1274
- const preview = previewTakomiRoutingPolicy(cwd, text, { scope: "global" });
1515
+ const availableModels = (() => {
1516
+ try {
1517
+ const available = (runtimeCtx as typeof runtimeCtx & { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } })?.modelRegistry?.getAvailable?.() ?? [];
1518
+ return available.map((model) => `${model.provider ? `${model.provider}/` : ""}${model.id ?? model.name ?? ""}`).filter(Boolean);
1519
+ } catch {
1520
+ return [];
1521
+ }
1522
+ })();
1523
+ const preview = previewTakomiRoutingPolicy(cwd, text, { scope: "global", availableModels });
1524
+ const activePolicy = await resolveTakomiRoutingPolicy(cwd);
1525
+ const replacementWarning = activePolicy.text && activePolicy.text.trim().length > preview.policy.trim().length * 2
1526
+ ? `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.`
1527
+ : "The supplied text replaces the policy file exactly; it is not merged with the current policy.";
1275
1528
  return { action: "transform", text: [
1276
- "Review this Takomi routing policy extraction before it is saved.",
1529
+ "Review this advisory Takomi model-routing guidance before it is saved.",
1277
1530
  "",
1278
1531
  "Rules:",
1279
- "- 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.",
1532
+ "- Providerless names such as Sol, Terra, and Luna are valid advisory routing concepts.",
1533
+ "- Do not infer executable providers, allowlists, fallbacks, or persona defaults from this prose.",
1534
+ "- Executable changes belong in takomi.routing settings through takomi_config_routing.",
1535
+ "- Canonical Takomi personas are: architect, designer, coder, worker, reviewer, orchestrator.",
1536
+ "- 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.",
1537
+ "- If correct and safe, call takomi_apply_routing_policy with scope=global and the complete intended policy text.",
1538
+ "- Ask only for unresolved provider/account choices or genuine ambiguity; do not ask for facts available in registry or files.",
1284
1539
  "",
1285
- "Deterministic extraction:",
1540
+ "Advisory preview:",
1286
1541
  renderRoutingPolicyPreview(preview),
1287
1542
  "",
1543
+ replacementWarning,
1544
+ "",
1545
+ 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.",
1546
+ "",
1288
1547
  "Original policy text:",
1289
1548
  "```",
1290
1549
  preview.policy,
@@ -1308,7 +1567,7 @@ ${stateJson}`
1308
1567
  state.modeReason = "explicit user request";
1309
1568
  const route = decideRoute(text.slice("use takomi ".length));
1310
1569
  if (route.stage) setStageAndWorkflow(state, route.stage, { preserveRole: state.role === "orchestrator" && route.stage === "genesis" });
1311
- else if (route.role !== "general") state.role = route.role;
1570
+ else state.role = route.role;
1312
1571
  return { action: "transform", text: `Use the Takomi runtime for this request: ${text.slice("use takomi ".length)}` };
1313
1572
  }
1314
1573
 
@@ -1451,6 +1710,8 @@ ${stateJson}`
1451
1710
  const entry = entries[i] as { type: string; customType?: string; data?: TakomiState };
1452
1711
  if (entry.type === "custom" && entry.customType === STATE_ENTRY && entry.data) {
1453
1712
  state = { ...DEFAULT_STATE, ...entry.data };
1713
+ const legacyRole = String((entry.data as { role?: unknown }).role ?? "general");
1714
+ state.role = legacyRole === "general" ? "general" : canonicalizeTakomiPersona(legacyRole as TakomiPersona | "design" | "code" | "review");
1454
1715
  break;
1455
1716
  }
1456
1717
  }