takomi 2.1.33 → 2.1.35
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/extensions/takomi-runtime/command-text.ts +2 -4
- package/.pi/extensions/takomi-runtime/commands.ts +15 -21
- package/.pi/extensions/takomi-runtime/context-panel.ts +37 -13
- package/.pi/extensions/takomi-runtime/index.ts +127 -53
- package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +35 -5
- package/.pi/extensions/takomi-runtime/routing-policy.ts +35 -7
- package/.pi/extensions/takomi-runtime/takomi-stats.js +13 -3
- package/.pi/extensions/takomi-runtime/ui.ts +18 -11
- package/.pi/extensions/takomi-subagents/index.ts +5 -3
- package/.pi/extensions/takomi-subagents/native-render.ts +73 -6
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +33 -10
- package/.pi/extensions/takomi-subagents/tool-runner.ts +62 -15
- package/assets/.agent/skills/photo-book-builder/SKILL.md +51 -7
- package/package.json +18 -2
- package/src/cli.js +49 -15
- package/src/harness.js +12 -42
- package/src/owned-tree.js +86 -0
- package/src/pi-harness.js +36 -14
- package/src/pi-installer.js +12 -36
- package/src/store.js +3 -40
- package/src/takomi-stats.js +14 -3
- package/src/utils.js +81 -44
|
@@ -35,9 +35,6 @@ const SUBCOMMAND_COMPLETIONS: Record<string, TakomiCompletion[]> = {
|
|
|
35
35
|
{ value: "status", label: "status", description: "Show active subagents" },
|
|
36
36
|
{ value: "on", label: "on", description: "Allow subagent delegation" },
|
|
37
37
|
{ value: "off", label: "off", description: "Disable subagent delegation" },
|
|
38
|
-
{ value: "expand", label: "expand", description: "Expand native tool results" },
|
|
39
|
-
{ value: "collapse", label: "collapse", description: "Collapse native tool results" },
|
|
40
|
-
{ value: "toggle", label: "toggle", description: "Toggle native tool result expansion" },
|
|
41
38
|
],
|
|
42
39
|
stats: [
|
|
43
40
|
{ value: "overview", label: "overview", description: "Show the full profile-card dashboard" },
|
|
@@ -98,7 +95,7 @@ export function commandHelp(): string {
|
|
|
98
95
|
"/takomi plan [title]",
|
|
99
96
|
"/takomi mode <direct|orchestrate|review>",
|
|
100
97
|
"/takomi gate <auto|review>",
|
|
101
|
-
"/takomi subagents <on|off|status
|
|
98
|
+
"/takomi subagents <on|off|status>",
|
|
102
99
|
"/takomi stats [overview|daily|models|projects|projects-full|sessions|sessions-full|tasks|tasks-full|tools|subagents|sources] [since 7d]",
|
|
103
100
|
"/takomi routing [show|where]",
|
|
104
101
|
"/takomi routing <policy text> # updates global policy",
|
|
@@ -134,6 +131,7 @@ export function statusText(state: TakomiRuntimeCommandState, subagentController:
|
|
|
134
131
|
return [
|
|
135
132
|
`Takomi: ${state.enabled ? "on" : "off"}`,
|
|
136
133
|
`Mode: ${state.role}`,
|
|
134
|
+
`Source: ${state.modeSource ?? "idle"}${state.modeReason ? ` (${state.modeReason})` : ""}`,
|
|
137
135
|
`Stage: ${state.stage ?? "-"}`,
|
|
138
136
|
`Workflow: ${state.workflow ?? "-"}`,
|
|
139
137
|
`Plan bias: ${state.planMode ? "on" : "off"}`,
|
|
@@ -20,6 +20,8 @@ export type TakomiRuntimeCommandState = {
|
|
|
20
20
|
workflow?: TakomiWorkflowId;
|
|
21
21
|
activeSessionId?: string;
|
|
22
22
|
subagentsEnabled: boolean;
|
|
23
|
+
modeSource?: "idle" | "manual" | "model" | "board";
|
|
24
|
+
modeReason?: string;
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
type RegisterTakomiCommandOptions = {
|
|
@@ -36,6 +38,8 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
|
|
|
36
38
|
async function handleStage(ctx: ExtensionCommandContext, stage: VibeLifecycleStage, prompt?: string): Promise<void> {
|
|
37
39
|
await options.updateState(ctx, () => {
|
|
38
40
|
options.getState().enabled = true;
|
|
41
|
+
options.getState().modeSource = "manual";
|
|
42
|
+
options.getState().modeReason = `/${stage}`;
|
|
39
43
|
options.setStageAndWorkflow(stage, { preserveRole: stage === "genesis" && options.getState().role === "orchestrator" });
|
|
40
44
|
options.getState().planMode = stage !== "build";
|
|
41
45
|
}, workflowPrompt(stage, prompt));
|
|
@@ -54,17 +58,27 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
|
|
|
54
58
|
state.autoOrch = false;
|
|
55
59
|
state.planMode = false;
|
|
56
60
|
state.role = "general";
|
|
61
|
+
state.stage = undefined;
|
|
62
|
+
state.workflow = undefined;
|
|
63
|
+
state.modeSource = "idle";
|
|
64
|
+
state.modeReason = undefined;
|
|
57
65
|
} else if (mode === "orchestrate") {
|
|
58
66
|
state.autoOrch = true;
|
|
59
67
|
state.planMode = true;
|
|
60
68
|
state.role = "orchestrator";
|
|
61
69
|
state.stage = hasGenesis ? "build" : "genesis";
|
|
62
70
|
state.workflow = hasGenesis ? "vibe-build" : "vibe-genesis";
|
|
71
|
+
state.modeSource = "manual";
|
|
72
|
+
state.modeReason = "/takomi mode orchestrate";
|
|
63
73
|
} else {
|
|
64
74
|
state.autoOrch = false;
|
|
65
75
|
state.planMode = true;
|
|
66
76
|
state.launchMode = "manual";
|
|
67
77
|
state.role = "review";
|
|
78
|
+
state.stage = undefined;
|
|
79
|
+
state.workflow = undefined;
|
|
80
|
+
state.modeSource = "manual";
|
|
81
|
+
state.modeReason = "/takomi mode review";
|
|
68
82
|
}
|
|
69
83
|
}, () => `Takomi mode set to ${mode}`);
|
|
70
84
|
}
|
|
@@ -198,27 +212,7 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
|
|
|
198
212
|
ctx.ui.notify(statusText(options.getState(), controller), controller.hasRuns() ? "info" : "warning");
|
|
199
213
|
return;
|
|
200
214
|
}
|
|
201
|
-
|
|
202
|
-
ctx.ui.setToolsExpanded(true);
|
|
203
|
-
ctx.ui.notify("Expanded native tool results for Takomi subagent output.", "info");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (action === "collapse") {
|
|
207
|
-
ctx.ui.setToolsExpanded(false);
|
|
208
|
-
ctx.ui.notify("Collapsed native tool results.", "info");
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (action === "toggle") {
|
|
212
|
-
const expanded = !ctx.ui.getToolsExpanded();
|
|
213
|
-
ctx.ui.setToolsExpanded(expanded);
|
|
214
|
-
ctx.ui.notify(`${expanded ? "Expanded" : "Collapsed"} native tool results.`, "info");
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (action === "next" || action === "prev") {
|
|
218
|
-
ctx.ui.notify("Takomi now uses Pi's native inline result UI. Subagent navigation is handled by the transcript/tool results.", "info");
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
ctx.ui.notify("Usage: /takomi subagents <on|off|status|expand|collapse|toggle>", "warning");
|
|
215
|
+
ctx.ui.notify("Usage: /takomi subagents <on|off|status>", "warning");
|
|
222
216
|
}
|
|
223
217
|
|
|
224
218
|
pi.registerCommand("takomi", {
|
|
@@ -206,6 +206,18 @@ function calculateActiveMs(intervals: ActivityInterval[]): number {
|
|
|
206
206
|
return mergeIntervals(intervals).reduce((total, interval) => total + (interval.end - interval.start), 0);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
function clampActiveMs(activeMs: number, sessionStart: number, now = Date.now()): number {
|
|
210
|
+
if (!Number.isFinite(activeMs) || activeMs <= 0) return 0;
|
|
211
|
+
const ageMs = Math.max(0, now - sessionStart);
|
|
212
|
+
return Math.min(activeMs, ageMs);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function boundedPendingStart(starts: number[], sessionStart: number, now = Date.now()): number | undefined {
|
|
216
|
+
const valid = starts.filter((start) => Number.isFinite(start) && start <= now);
|
|
217
|
+
if (valid.length === 0) return undefined;
|
|
218
|
+
return Math.max(sessionStart, Math.min(...valid));
|
|
219
|
+
}
|
|
220
|
+
|
|
209
221
|
function formatDisplayPath(filePath: string, cwd?: string): string {
|
|
210
222
|
const normalized = filePath.replace(/^@/, "");
|
|
211
223
|
const absolute = path.isAbsolute(normalized) ? normalized : undefined;
|
|
@@ -312,11 +324,13 @@ class ContextPanelComponent implements Component {
|
|
|
312
324
|
lines.push("");
|
|
313
325
|
|
|
314
326
|
if (ctx) {
|
|
315
|
-
const
|
|
327
|
+
const now = Date.now();
|
|
328
|
+
const ageMs = Math.max(0, now - state.sessionStart);
|
|
329
|
+
const age = formatDuration(ageMs);
|
|
316
330
|
const pendingStarts = Object.values(state.pendingToolStarts ?? {});
|
|
317
|
-
const pendingStart = pendingStarts.
|
|
318
|
-
const
|
|
319
|
-
const active = formatDuration(
|
|
331
|
+
const pendingStart = boundedPendingStart(pendingStarts, state.sessionStart, now);
|
|
332
|
+
const rawActiveMs = pendingStart !== undefined ? state.activeMs + Math.max(0, now - pendingStart) : state.activeMs;
|
|
333
|
+
const active = formatDuration(clampActiveMs(rawActiveMs, state.sessionStart, now));
|
|
320
334
|
lines.push(`${pad}${theme.fg("dim", "Age:")} ${theme.fg("muted", age)}`);
|
|
321
335
|
lines.push(`${pad}${theme.fg("dim", "Active:")} ${theme.fg("muted", active)}`);
|
|
322
336
|
|
|
@@ -417,15 +431,18 @@ export class TakomiContextPanel {
|
|
|
417
431
|
this.requestRender?.();
|
|
418
432
|
}
|
|
419
433
|
|
|
420
|
-
private recomputeActiveMs(): void {
|
|
421
|
-
this.state.activeMs = calculateActiveMs(this.state.activityIntervals);
|
|
434
|
+
private recomputeActiveMs(now = Date.now()): void {
|
|
435
|
+
this.state.activeMs = clampActiveMs(calculateActiveMs(this.state.activityIntervals), this.state.sessionStart, now);
|
|
422
436
|
}
|
|
423
437
|
|
|
424
438
|
private addActivityInterval(start: number, end: number): void {
|
|
425
|
-
|
|
426
|
-
this.state.
|
|
427
|
-
|
|
428
|
-
|
|
439
|
+
const now = Date.now();
|
|
440
|
+
const boundedStart = Math.max(this.state.sessionStart, start);
|
|
441
|
+
const boundedEnd = Math.min(now, end);
|
|
442
|
+
if (!Number.isFinite(boundedStart) || !Number.isFinite(boundedEnd) || boundedEnd <= boundedStart) return;
|
|
443
|
+
this.state.activityIntervals.push({ start: boundedStart, end: boundedEnd });
|
|
444
|
+
this.recomputeActiveMs(now);
|
|
445
|
+
this.state.lastActivityAt = Math.max(this.state.lastActivityAt, boundedEnd);
|
|
429
446
|
}
|
|
430
447
|
|
|
431
448
|
private noteActivity(timestamp = Date.now(), allowLongGap = false): void {
|
|
@@ -564,10 +581,13 @@ export class TakomiContextPanel {
|
|
|
564
581
|
lastActivityPoint = ts;
|
|
565
582
|
}
|
|
566
583
|
|
|
584
|
+
const now = Date.now();
|
|
567
585
|
next.sessionStart = firstSessionTs ?? next.sessionStart;
|
|
568
|
-
next.activityIntervals = intervals
|
|
569
|
-
|
|
570
|
-
|
|
586
|
+
next.activityIntervals = intervals
|
|
587
|
+
.map((interval) => ({ start: Math.max(next.sessionStart, interval.start), end: Math.min(now, interval.end) }))
|
|
588
|
+
.filter((interval) => Number.isFinite(interval.start) && Number.isFinite(interval.end) && interval.end > interval.start);
|
|
589
|
+
next.activeMs = clampActiveMs(calculateActiveMs(next.activityIntervals), next.sessionStart, now);
|
|
590
|
+
next.lastActivityAt = Math.min(now, Math.max(next.sessionStart, lastActivityPoint ?? next.sessionStart));
|
|
571
591
|
|
|
572
592
|
this.state = next;
|
|
573
593
|
this.requestRender?.();
|
|
@@ -671,6 +691,10 @@ export function wireContextPanel(pi: ExtensionAPI, panel: TakomiContextPanel): v
|
|
|
671
691
|
panel.rebuildFromSession(ctx);
|
|
672
692
|
});
|
|
673
693
|
|
|
694
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
695
|
+
panel.rebuildFromSession(ctx);
|
|
696
|
+
});
|
|
697
|
+
|
|
674
698
|
pi.registerShortcut("alt+c", {
|
|
675
699
|
description: "Toggle Takomi context panel",
|
|
676
700
|
handler: async (ctx) => {
|
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
} from "./profile";
|
|
59
59
|
import { installTakomiRoutingPolicy, previewTakomiRoutingPolicy, renderRoutingPolicyPreview, resolveTakomiRoutingPolicy } from "./routing-policy";
|
|
60
60
|
|
|
61
|
+
type TakomiModeSource = "idle" | "manual" | "model" | "board";
|
|
62
|
+
|
|
61
63
|
type TakomiState = {
|
|
62
64
|
enabled: boolean;
|
|
63
65
|
autoOrch: boolean;
|
|
@@ -69,15 +71,18 @@ type TakomiState = {
|
|
|
69
71
|
activeSessionId?: string;
|
|
70
72
|
subagentsEnabled: boolean;
|
|
71
73
|
lastFullPromptKey?: string;
|
|
74
|
+
modeSource?: TakomiModeSource;
|
|
75
|
+
modeReason?: string;
|
|
72
76
|
};
|
|
73
77
|
|
|
74
78
|
const DEFAULT_STATE: TakomiState = {
|
|
75
79
|
enabled: true,
|
|
76
|
-
autoOrch:
|
|
80
|
+
autoOrch: false,
|
|
77
81
|
launchMode: "auto",
|
|
78
82
|
planMode: false,
|
|
79
83
|
role: "general",
|
|
80
84
|
subagentsEnabled: true,
|
|
85
|
+
modeSource: "idle",
|
|
81
86
|
};
|
|
82
87
|
|
|
83
88
|
const STATE_ENTRY = "takomi-runtime-state";
|
|
@@ -97,6 +102,8 @@ const ThinkingSchema = Type.Union([
|
|
|
97
102
|
Type.Literal("xhigh"),
|
|
98
103
|
]);
|
|
99
104
|
|
|
105
|
+
const TakomiModeSchema = StringEnum(["idle", "code", "orchestrate", "review", "genesis", "design", "build"] as const);
|
|
106
|
+
|
|
100
107
|
function cloneState(state: TakomiState): TakomiState {
|
|
101
108
|
return { ...state };
|
|
102
109
|
}
|
|
@@ -111,6 +118,8 @@ function formatState(state: TakomiState): string {
|
|
|
111
118
|
`launch=${state.launchMode}`,
|
|
112
119
|
`plan=${state.planMode ? "on" : "off"}`,
|
|
113
120
|
`subagents=${state.subagentsEnabled ? "on" : "off"}`,
|
|
121
|
+
`source=${state.modeSource ?? "idle"}`,
|
|
122
|
+
state.modeReason ? `reason=${state.modeReason}` : "",
|
|
114
123
|
state.activeSessionId ? `session=${state.activeSessionId}` : "",
|
|
115
124
|
].filter(Boolean).join(" | ");
|
|
116
125
|
}
|
|
@@ -766,6 +775,8 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
766
775
|
state.stage = "genesis";
|
|
767
776
|
state.workflow = "vibe-genesis";
|
|
768
777
|
state.role = "orchestrator";
|
|
778
|
+
state.modeSource = "manual";
|
|
779
|
+
state.modeReason = "/takomi plan";
|
|
769
780
|
});
|
|
770
781
|
return `Takomi plan created session ${session.sessionId}\nMaster plan: ${paths.masterPlan}`;
|
|
771
782
|
},
|
|
@@ -783,34 +794,82 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
783
794
|
},
|
|
784
795
|
});
|
|
785
796
|
|
|
786
|
-
pi.registerShortcut("alt+t", {
|
|
787
|
-
description: "Toggle native tool result expansion",
|
|
788
|
-
handler: async (ctx) => {
|
|
789
|
-
const expanded = !ctx.ui.getToolsExpanded();
|
|
790
|
-
ctx.ui.setToolsExpanded(expanded);
|
|
791
|
-
ctx.ui.notify(`${expanded ? "Expanded" : "Collapsed"} native tool results.`, "info");
|
|
792
|
-
},
|
|
793
|
-
});
|
|
794
797
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
798
|
+
async function applyTakomiMode(ctx: ExtensionContext, mode: string, source: TakomiModeSource, reason?: string): Promise<string> {
|
|
799
|
+
const hasGenesis = await hasGenesisArtifacts(ctx.cwd);
|
|
800
|
+
state.enabled = true;
|
|
801
|
+
state.modeSource = source;
|
|
802
|
+
state.modeReason = reason?.trim() || undefined;
|
|
803
|
+
|
|
804
|
+
switch (mode) {
|
|
805
|
+
case "idle":
|
|
806
|
+
state.modeSource = "idle";
|
|
807
|
+
state.modeReason = undefined;
|
|
808
|
+
state.autoOrch = false;
|
|
809
|
+
state.planMode = false;
|
|
810
|
+
state.role = "general";
|
|
811
|
+
state.stage = undefined;
|
|
812
|
+
state.workflow = undefined;
|
|
813
|
+
break;
|
|
814
|
+
case "code":
|
|
815
|
+
state.autoOrch = false;
|
|
816
|
+
state.planMode = false;
|
|
817
|
+
state.role = "code";
|
|
818
|
+
state.stage = undefined;
|
|
819
|
+
state.workflow = undefined;
|
|
820
|
+
break;
|
|
821
|
+
case "review":
|
|
822
|
+
state.autoOrch = false;
|
|
823
|
+
state.planMode = true;
|
|
824
|
+
state.launchMode = "manual";
|
|
825
|
+
state.role = "review";
|
|
826
|
+
state.stage = undefined;
|
|
827
|
+
state.workflow = undefined;
|
|
828
|
+
break;
|
|
829
|
+
case "orchestrate":
|
|
830
|
+
state.autoOrch = true;
|
|
831
|
+
state.planMode = true;
|
|
832
|
+
state.role = "orchestrator";
|
|
833
|
+
state.stage = hasGenesis ? "build" : "genesis";
|
|
834
|
+
state.workflow = hasGenesis ? "vibe-build" : "vibe-genesis";
|
|
835
|
+
break;
|
|
836
|
+
case "genesis":
|
|
837
|
+
case "design":
|
|
838
|
+
case "build":
|
|
839
|
+
setStageAndWorkflow(state, mode);
|
|
840
|
+
state.autoOrch = mode === "build";
|
|
841
|
+
state.planMode = mode !== "build";
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
802
844
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
808
|
-
}
|
|
845
|
+
persistState();
|
|
846
|
+
syncContextPanelState();
|
|
847
|
+
await refreshUi(ctx, state);
|
|
848
|
+
const label = state.modeSource === "idle" ? "idle" : `${state.modeSource}:${state.stage ?? state.role}`;
|
|
849
|
+
return `Takomi mode set to ${label}${state.modeReason ? ` (${state.modeReason})` : ""}.`;
|
|
850
|
+
}
|
|
809
851
|
|
|
810
|
-
pi.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
852
|
+
pi.registerTool({
|
|
853
|
+
name: "takomi_mode",
|
|
854
|
+
label: "Takomi Mode",
|
|
855
|
+
description: "Set or clear the visible Takomi runtime mode after the active model decides a task benefits from code, review, lifecycle, or orchestration handling.",
|
|
856
|
+
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.",
|
|
857
|
+
promptGuidelines: [
|
|
858
|
+
"Let the user's request drive the choice; do not switch modes just because a vague word like code/review/build appears alone.",
|
|
859
|
+
"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.",
|
|
860
|
+
"Use mode=idle when the user asks normal non-coding/non-Takomi questions and Takomi should get out of the way.",
|
|
861
|
+
],
|
|
862
|
+
parameters: Type.Object({
|
|
863
|
+
mode: TakomiModeSchema,
|
|
864
|
+
reason: Type.Optional(Type.String({ description: "Short human-readable reason for the switch" })),
|
|
865
|
+
}),
|
|
866
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
867
|
+
const text = await applyTakomiMode(ctx, params.mode, "model", params.reason);
|
|
868
|
+
ctx.ui.notify(text, "info");
|
|
869
|
+
return {
|
|
870
|
+
content: [{ type: "text", text }],
|
|
871
|
+
details: { mode: params.mode, source: state.modeSource, reason: state.modeReason, role: state.role, stage: state.stage, workflow: state.workflow },
|
|
872
|
+
};
|
|
814
873
|
},
|
|
815
874
|
});
|
|
816
875
|
|
|
@@ -1007,6 +1066,12 @@ ${stateJson}`
|
|
|
1007
1066
|
}
|
|
1008
1067
|
}
|
|
1009
1068
|
sessionState.tasks[idx] = nextTask;
|
|
1069
|
+
state.activeSessionId = params.sessionId;
|
|
1070
|
+
state.modeSource = state.modeSource === "idle" ? "board" : state.modeSource;
|
|
1071
|
+
state.modeReason = state.modeReason ?? "board task update";
|
|
1072
|
+
persistState();
|
|
1073
|
+
syncContextPanelState();
|
|
1074
|
+
await refreshUi(ctx, state);
|
|
1010
1075
|
const nextState = buildSessionState(
|
|
1011
1076
|
sessionState.sessionId,
|
|
1012
1077
|
sessionState.title,
|
|
@@ -1062,8 +1127,11 @@ ${stateJson}`
|
|
|
1062
1127
|
}
|
|
1063
1128
|
}
|
|
1064
1129
|
state.activeSessionId = nextState.sessionId;
|
|
1130
|
+
state.modeSource = "board";
|
|
1131
|
+
state.modeReason = `expanded ${params.stage} stage`;
|
|
1065
1132
|
persistState();
|
|
1066
1133
|
syncContextPanelState();
|
|
1134
|
+
await refreshUi(ctx, state);
|
|
1067
1135
|
|
|
1068
1136
|
return {
|
|
1069
1137
|
content: [{ type: "text", text: `Expanded ${params.stage} stage in session ${nextState.sessionId}.\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks)}` }],
|
|
@@ -1104,8 +1172,11 @@ ${stateJson}`
|
|
|
1104
1172
|
state.role = "orchestrator";
|
|
1105
1173
|
state.stage = nextState.lifecycle.genesis.status === "completed" ? "build" : "genesis";
|
|
1106
1174
|
state.workflow = state.stage === "genesis" ? "vibe-genesis" : "vibe-build";
|
|
1175
|
+
state.modeSource = "board";
|
|
1176
|
+
state.modeReason = "orchestrator session";
|
|
1107
1177
|
persistState();
|
|
1108
1178
|
syncContextPanelState();
|
|
1179
|
+
await refreshUi(ctx, state);
|
|
1109
1180
|
|
|
1110
1181
|
return {
|
|
1111
1182
|
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."}` }],
|
|
@@ -1152,11 +1223,15 @@ ${stateJson}`
|
|
|
1152
1223
|
|
|
1153
1224
|
if (lowered === "use takomi") {
|
|
1154
1225
|
state.enabled = true;
|
|
1226
|
+
state.modeSource = "manual";
|
|
1227
|
+
state.modeReason = "explicit user request";
|
|
1155
1228
|
return { action: "transform", text: "Use the Takomi runtime, identify the correct lifecycle stage, and proceed accordingly." };
|
|
1156
1229
|
}
|
|
1157
1230
|
|
|
1158
1231
|
if (lowered.startsWith("use takomi ")) {
|
|
1159
1232
|
state.enabled = true;
|
|
1233
|
+
state.modeSource = "manual";
|
|
1234
|
+
state.modeReason = "explicit user request";
|
|
1160
1235
|
const route = decideRoute(text.slice("use takomi ".length));
|
|
1161
1236
|
if (route.stage) setStageAndWorkflow(state, route.stage, { preserveRole: state.role === "orchestrator" && route.stage === "genesis" });
|
|
1162
1237
|
else if (route.role !== "general") state.role = route.role;
|
|
@@ -1164,14 +1239,20 @@ ${stateJson}`
|
|
|
1164
1239
|
}
|
|
1165
1240
|
|
|
1166
1241
|
if (/\bvibe genesis\b/i.test(text)) {
|
|
1242
|
+
state.modeSource = "manual";
|
|
1243
|
+
state.modeReason = "vibe genesis";
|
|
1167
1244
|
setStageAndWorkflow(state, "genesis", { preserveRole: state.role === "orchestrator" });
|
|
1168
1245
|
return { action: "transform", text };
|
|
1169
1246
|
}
|
|
1170
1247
|
if (/\bvibe design\b/i.test(text)) {
|
|
1248
|
+
state.modeSource = "manual";
|
|
1249
|
+
state.modeReason = "vibe design";
|
|
1171
1250
|
setStageAndWorkflow(state, "design");
|
|
1172
1251
|
return { action: "transform", text };
|
|
1173
1252
|
}
|
|
1174
1253
|
if (/\bvibe build\b/i.test(text)) {
|
|
1254
|
+
state.modeSource = "manual";
|
|
1255
|
+
state.modeReason = "vibe build";
|
|
1175
1256
|
setStageAndWorkflow(state, "build");
|
|
1176
1257
|
return { action: "transform", text };
|
|
1177
1258
|
}
|
|
@@ -1180,43 +1261,36 @@ ${stateJson}`
|
|
|
1180
1261
|
});
|
|
1181
1262
|
|
|
1182
1263
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
1183
|
-
if (!state.enabled) return;
|
|
1264
|
+
if (!state.enabled || (state.modeSource ?? "idle") === "idle") return;
|
|
1184
1265
|
|
|
1185
1266
|
let effectiveState = cloneState(state);
|
|
1186
1267
|
const runtimeCwd = ctx.cwd;
|
|
1187
1268
|
const genesisExists = await hasGenesisArtifacts(runtimeCwd);
|
|
1188
1269
|
const route = decideRoute(event.prompt);
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
effectiveState.workflow = genesisExists ? "vibe-build" : "vibe-genesis";
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
const shouldHonorRoute = route.stage || route.role !== "general" || route.sessionRecommendation !== "none";
|
|
1196
|
-
if (shouldHonorRoute && route.stage) {
|
|
1197
|
-
effectiveState.stage = route.stage;
|
|
1198
|
-
effectiveState.workflow = route.workflow;
|
|
1199
|
-
effectiveState.role = effectiveState.role === "orchestrator" && route.stage === "genesis" ? "orchestrator" : route.role;
|
|
1200
|
-
} else if (shouldHonorRoute && route.role !== "general") {
|
|
1201
|
-
effectiveState.role = route.role;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
let routingNote = route.reason;
|
|
1270
|
+
let routingNote = state.modeReason
|
|
1271
|
+
? `Takomi mode selected by ${state.modeSource}: ${state.modeReason}.`
|
|
1272
|
+
: `Takomi mode selected by ${state.modeSource ?? "runtime"}.`;
|
|
1205
1273
|
const explicitLifecycleWaiver = /skip genesis|waive genesis|genesis complete|already have (a )?(prd|requirements)|design complete|jump straight to build/i.test(event.prompt);
|
|
1206
|
-
const orchestrationActive = effectiveState.role === "orchestrator"
|
|
1274
|
+
const orchestrationActive = effectiveState.role === "orchestrator";
|
|
1207
1275
|
if (!genesisExists && orchestrationActive && !explicitLifecycleWaiver) {
|
|
1208
1276
|
effectiveState.stage = "genesis";
|
|
1209
1277
|
effectiveState.workflow = "vibe-genesis";
|
|
1210
1278
|
routingNote = "Blank project detected; orchestrator remains in control and must honor Genesis → Design → Build.";
|
|
1211
1279
|
}
|
|
1280
|
+
if (effectiveState.stage !== state.stage || effectiveState.workflow !== state.workflow || effectiveState.role !== state.role) {
|
|
1281
|
+
state.role = effectiveState.role;
|
|
1282
|
+
state.stage = effectiveState.stage;
|
|
1283
|
+
state.workflow = effectiveState.workflow;
|
|
1284
|
+
}
|
|
1212
1285
|
|
|
1213
1286
|
const promptKey = `${effectiveState.role}:${effectiveState.workflow ?? "none"}`;
|
|
1214
1287
|
const includeFullWorkflow = Boolean(effectiveState.workflow && effectiveState.lastFullPromptKey !== promptKey);
|
|
1215
1288
|
if (includeFullWorkflow) {
|
|
1216
1289
|
state.lastFullPromptKey = promptKey;
|
|
1217
|
-
persistState();
|
|
1218
|
-
syncContextPanelState();
|
|
1219
1290
|
}
|
|
1291
|
+
persistState();
|
|
1292
|
+
syncContextPanelState();
|
|
1293
|
+
await refreshUi(ctx, state);
|
|
1220
1294
|
|
|
1221
1295
|
const routingPolicy = await resolveTakomiRoutingPolicy(runtimeCwd);
|
|
1222
1296
|
const optionalFeatureContext = (() => {
|
|
@@ -1263,14 +1337,14 @@ ${stateJson}`
|
|
|
1263
1337
|
`Execution mode: ${route.executionMode}. Session recommendation: ${route.sessionRecommendation}.`,
|
|
1264
1338
|
`Takomi execution gate: ${effectiveState.launchMode === "manual" ? "review" : "auto"}. In review gate mode, show the delegation plan before launching and return to the user after each task with results, verification guidance, and the recommended next step.`,
|
|
1265
1339
|
!effectiveState.subagentsEnabled ? "Takomi subagents are disabled for this session. Do not call takomi_subagent or subagent until the user enables subagents." : "",
|
|
1266
|
-
!genesisExists ? "Project foundation is missing or incomplete. Do not skip Genesis unless the user explicitly waives it." : "",
|
|
1267
|
-
"
|
|
1268
|
-
"Task fan-out is flexible. Do not force exactly three tasks; decompose Genesis, Design, and Build work to fit the actual scope.",
|
|
1269
|
-
"A new orchestration session should usually begin with one Genesis foundation task that creates or updates the required markdown artifacts, then expand later stages only when the scope justifies it.",
|
|
1270
|
-
"If a follow-up request is small, one-shot it. If it is multi-part or large, create or expand an orchestration session instead of pretending it is a single task.",
|
|
1340
|
+
orchestrationActive && !genesisExists ? "Project foundation is missing or incomplete. Do not skip Genesis unless the user explicitly waives it." : "",
|
|
1341
|
+
"Do not escalate to orchestration or review just because this is coding-related; stay in the selected Takomi mode unless the user asks or a durable board/session is genuinely needed.",
|
|
1342
|
+
orchestrationActive ? "Task fan-out is flexible. Do not force exactly three tasks; decompose Genesis, Design, and Build work to fit the actual scope." : "",
|
|
1343
|
+
orchestrationActive ? "A new orchestration session should usually begin with one Genesis foundation task that creates or updates the required markdown artifacts, then expand later stages only when the scope justifies it." : "",
|
|
1344
|
+
orchestrationActive ? "If a follow-up request is small, one-shot it. If it is multi-part or large, create or expand an orchestration session instead of pretending it is a single task." : "",
|
|
1271
1345
|
"Before any Takomi subagent dispatch or model override, use the injected Pi model-registry context and project routing policy. Prefer provider-qualified model IDs. Do not run `pi --list-models` unless the registry context is missing or the user asks for a visible diagnostic.",
|
|
1272
|
-
"When useful, state the current Takomi stage and the recommended next
|
|
1273
|
-
effectiveState.stage === "build"
|
|
1346
|
+
"When useful, state the current Takomi mode/stage and the recommended next step.",
|
|
1347
|
+
orchestrationActive && effectiveState.stage === "build"
|
|
1274
1348
|
? "For build orchestration, use takomi_subagent to dispatch work to specialist subagents, then record the result on takomi_board; reuse the same conversation id when sending fixes back to the agent."
|
|
1275
1349
|
: "",
|
|
1276
1350
|
].filter(Boolean);
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from "./routing-policy";
|
|
12
12
|
|
|
13
13
|
export const TAKOMI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
14
|
+
const PROJECT_TAKOMI_POLICY_ROOT_RELATIVE = path.join(".pi", "takomi");
|
|
15
|
+
const MAX_POLICY_BYTES = 128 * 1024;
|
|
14
16
|
|
|
15
17
|
type Settings = {
|
|
16
18
|
takomi?: { modelRoutingPolicyFile?: string };
|
|
@@ -140,6 +142,8 @@ function collectModelsFromPolicy(text: string): string[] {
|
|
|
140
142
|
|
|
141
143
|
function readPolicyTextSync(filePath: string): string | undefined {
|
|
142
144
|
try {
|
|
145
|
+
const info = fs.statSync(filePath);
|
|
146
|
+
if (!info.isFile() || info.size > MAX_POLICY_BYTES) return undefined;
|
|
143
147
|
const text = fs.readFileSync(filePath, "utf8").trim();
|
|
144
148
|
return text || undefined;
|
|
145
149
|
} catch {
|
|
@@ -147,18 +151,44 @@ function readPolicyTextSync(filePath: string): string | undefined {
|
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
function isPathInside(root: string, candidate: string): boolean {
|
|
155
|
+
const relative = path.relative(root, candidate);
|
|
156
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveSafeProjectPolicyPathSync(cwd: string, configured: string): string | undefined {
|
|
160
|
+
if (!configured) return undefined;
|
|
161
|
+
|
|
162
|
+
const projectPolicyRoot = path.resolve(cwd, PROJECT_TAKOMI_POLICY_ROOT_RELATIVE);
|
|
163
|
+
const resolvedPath = path.isAbsolute(configured) ? path.resolve(configured) : path.resolve(cwd, configured);
|
|
164
|
+
if (!isPathInside(projectPolicyRoot, resolvedPath)) return undefined;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const realCwd = fs.realpathSync(cwd);
|
|
168
|
+
const realRoot = fs.realpathSync(projectPolicyRoot);
|
|
169
|
+
const realFile = fs.realpathSync(resolvedPath);
|
|
170
|
+
if (!isPathInside(realCwd, realRoot)) return undefined;
|
|
171
|
+
if (!isPathInside(realRoot, realFile)) return undefined;
|
|
172
|
+
return realFile;
|
|
173
|
+
} catch {
|
|
174
|
+
return resolvedPath;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
150
178
|
function resolvePolicySync(cwd: string): { policyPath?: string; text?: string } {
|
|
151
179
|
const projectSettings = readSettingsFileSync(path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE));
|
|
152
180
|
const projectTakomi = asRecord(projectSettings.takomi);
|
|
153
181
|
const configuredProject = typeof projectTakomi.modelRoutingPolicyFile === "string"
|
|
154
182
|
? projectTakomi.modelRoutingPolicyFile
|
|
155
183
|
: TAKOMI_ROUTING_POLICY_RELATIVE;
|
|
156
|
-
const configuredProjectPath =
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
const configuredProjectPath = resolveSafeProjectPolicyPathSync(cwd, configuredProject);
|
|
185
|
+
if (configuredProjectPath) {
|
|
186
|
+
const configuredProjectText = readPolicyTextSync(configuredProjectPath);
|
|
187
|
+
if (configuredProjectText) return { policyPath: configuredProjectPath, text: configuredProjectText };
|
|
188
|
+
}
|
|
159
189
|
|
|
160
|
-
const defaultProjectPath =
|
|
161
|
-
if (path.resolve(defaultProjectPath) !== path.resolve(configuredProjectPath)) {
|
|
190
|
+
const defaultProjectPath = resolveSafeProjectPolicyPathSync(cwd, TAKOMI_ROUTING_POLICY_RELATIVE);
|
|
191
|
+
if (defaultProjectPath && path.resolve(defaultProjectPath) !== path.resolve(configuredProjectPath ?? "")) {
|
|
162
192
|
const defaultProjectText = readPolicyTextSync(defaultProjectPath);
|
|
163
193
|
if (defaultProjectText) return { policyPath: defaultProjectPath, text: defaultProjectText };
|
|
164
194
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -14,6 +14,8 @@ export const BUNDLED_TAKOMI_ROUTING_POLICY_PATH = path.resolve(
|
|
|
14
14
|
"takomi",
|
|
15
15
|
"model-routing.md",
|
|
16
16
|
);
|
|
17
|
+
const PROJECT_TAKOMI_POLICY_ROOT_RELATIVE = path.join(".pi", "takomi");
|
|
18
|
+
const MAX_POLICY_BYTES = 128 * 1024;
|
|
17
19
|
|
|
18
20
|
export type RoutingPolicyInstallResult = {
|
|
19
21
|
policyPath: string;
|
|
@@ -56,6 +58,8 @@ async function readJsonObject(filePath: string): Promise<JsonObject> {
|
|
|
56
58
|
|
|
57
59
|
async function readPolicyText(filePath: string): Promise<string | undefined> {
|
|
58
60
|
try {
|
|
61
|
+
const info = await stat(filePath);
|
|
62
|
+
if (!info.isFile() || info.size > MAX_POLICY_BYTES) return undefined;
|
|
59
63
|
const text = (await readFile(filePath, "utf8")).trim();
|
|
60
64
|
return text || undefined;
|
|
61
65
|
} catch {
|
|
@@ -63,6 +67,28 @@ async function readPolicyText(filePath: string): Promise<string | undefined> {
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
function isPathInside(root: string, candidate: string): boolean {
|
|
71
|
+
const relative = path.relative(root, candidate);
|
|
72
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function resolveSafeProjectPolicyPath(cwd: string, configured: string): Promise<string | undefined> {
|
|
76
|
+
if (!configured) return undefined;
|
|
77
|
+
|
|
78
|
+
const projectPolicyRoot = path.resolve(cwd, PROJECT_TAKOMI_POLICY_ROOT_RELATIVE);
|
|
79
|
+
const resolvedPath = path.isAbsolute(configured) ? path.resolve(configured) : path.resolve(cwd, configured);
|
|
80
|
+
if (!isPathInside(projectPolicyRoot, resolvedPath)) return undefined;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const [realCwd, realRoot, realFile] = await Promise.all([realpath(cwd), realpath(projectPolicyRoot), realpath(resolvedPath)]);
|
|
84
|
+
if (!isPathInside(realCwd, realRoot)) return undefined;
|
|
85
|
+
if (!isPathInside(realRoot, realFile)) return undefined;
|
|
86
|
+
return realFile;
|
|
87
|
+
} catch {
|
|
88
|
+
return resolvedPath;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
66
92
|
function extractQuotedPolicy(text: string): string {
|
|
67
93
|
const triple = text.match(/"""([\s\S]*?)"""|```(?:\w+)?\s*([\s\S]*?)```/);
|
|
68
94
|
const raw = (triple?.[1] ?? triple?.[2] ?? text).trim();
|
|
@@ -133,14 +159,16 @@ export async function resolveTakomiRoutingPolicy(cwd: string): Promise<ResolvedR
|
|
|
133
159
|
const configuredProject = typeof projectTakomi.modelRoutingPolicyFile === "string"
|
|
134
160
|
? projectTakomi.modelRoutingPolicyFile
|
|
135
161
|
: TAKOMI_ROUTING_POLICY_RELATIVE;
|
|
136
|
-
const configuredProjectPath =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
const configuredProjectPath = await resolveSafeProjectPolicyPath(cwd, configuredProject);
|
|
163
|
+
if (configuredProjectPath) {
|
|
164
|
+
const configuredProjectText = await readPolicyText(configuredProjectPath);
|
|
165
|
+
if (configuredProjectText) {
|
|
166
|
+
return { source: "project", policyPath: configuredProjectPath, text: configuredProjectText };
|
|
167
|
+
}
|
|
140
168
|
}
|
|
141
169
|
|
|
142
|
-
const defaultProjectPath =
|
|
143
|
-
if (path.resolve(defaultProjectPath) !== path.resolve(configuredProjectPath)) {
|
|
170
|
+
const defaultProjectPath = await resolveSafeProjectPolicyPath(cwd, TAKOMI_ROUTING_POLICY_RELATIVE);
|
|
171
|
+
if (defaultProjectPath && path.resolve(defaultProjectPath) !== path.resolve(configuredProjectPath ?? "")) {
|
|
144
172
|
const defaultProjectText = await readPolicyText(defaultProjectPath);
|
|
145
173
|
if (defaultProjectText) {
|
|
146
174
|
return { source: "project", policyPath: defaultProjectPath, text: defaultProjectText };
|