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.
- package/.pi/README.md +9 -8
- package/.pi/agents/architect.md +2 -2
- package/.pi/agents/designer.md +2 -2
- package/.pi/agents/worker.md +32 -0
- package/.pi/extensions/oauth-router/commands.ts +66 -35
- package/.pi/extensions/oauth-router/index.ts +51 -7
- package/.pi/extensions/oauth-router/report-ui.ts +205 -0
- package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
- package/.pi/extensions/takomi-context-manager/index.ts +3 -2
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +50 -117
- package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
- package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
- package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
- package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
- package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
- package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
- package/.pi/extensions/takomi-context-manager/types.ts +6 -0
- package/.pi/extensions/takomi-runtime/command-text.ts +5 -4
- package/.pi/extensions/takomi-runtime/commands.ts +58 -25
- package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
- package/.pi/extensions/takomi-runtime/index.ts +385 -124
- package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +262 -326
- package/.pi/extensions/takomi-runtime/profile.ts +9 -8
- package/.pi/extensions/takomi-runtime/routing-policy.ts +97 -66
- package/.pi/extensions/takomi-runtime/shared.ts +7 -30
- package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
- package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
- package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
- package/.pi/extensions/takomi-subagents/agents.ts +9 -0
- package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
- package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
- package/.pi/extensions/takomi-subagents/index.ts +50 -1
- package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +62 -47
- package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
- package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
- package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
- package/.pi/extensions/takomi-subagents/tool-runner.ts +198 -29
- package/.pi/settings.json +39 -36
- package/.pi/takomi/model-routing.md +288 -3
- package/.pi/takomi-profile.json +54 -50
- package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
- package/package.json +4 -3
- package/src/pi-takomi-core/orchestration.ts +39 -21
- package/src/pi-takomi-core/routing.ts +8 -8
- package/src/pi-takomi-core/types.ts +35 -5
- package/src/pi-takomi-core/workflows.ts +86 -45
- package/src/skills-catalog.js +2 -202
- 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 {
|
|
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", "
|
|
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,
|
|
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 "
|
|
237
|
+
case "designer":
|
|
216
238
|
return [
|
|
217
|
-
"You are operating
|
|
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 "
|
|
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 "
|
|
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 "
|
|
243
|
-
case "
|
|
244
|
-
case "
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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:
|
|
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
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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,
|
|
770
|
+
nextTasks.push(createTask(id, task.title, persona, {
|
|
638
771
|
stage,
|
|
639
772
|
workflow: task.workflow,
|
|
640
773
|
parentTaskId: task.parentTaskId,
|
|
641
|
-
preferredAgent:
|
|
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
|
-
|
|
670
|
-
|
|
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 = "
|
|
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 = "
|
|
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
|
-
|
|
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
|
|
930
|
-
promptSnippet: "Optional:
|
|
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: "
|
|
954
|
-
promptSnippet: "Save reviewed
|
|
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
|
-
"
|
|
957
|
-
"
|
|
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
|
|
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
|
-
|
|
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(["
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
|
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
|
|
1529
|
+
"Review this advisory Takomi model-routing guidance before it is saved.",
|
|
1277
1530
|
"",
|
|
1278
1531
|
"Rules:",
|
|
1279
|
-
"-
|
|
1280
|
-
"-
|
|
1281
|
-
"-
|
|
1282
|
-
"-
|
|
1283
|
-
"- If
|
|
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
|
-
"
|
|
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
|
|
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
|
}
|