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