takomi 2.1.43 → 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/README.md +3 -3
- package/.pi/extensions/oauth-router/commands.ts +66 -39
- package/.pi/extensions/oauth-router/config.ts +34 -34
- package/.pi/extensions/oauth-router/index.ts +51 -10
- package/.pi/extensions/oauth-router/report-ui.ts +205 -0
- package/.pi/extensions/oauth-router/scripts/vibe-verify.py +5 -5
- 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 +20 -9
- 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 +133 -56
- 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-harness.js +41 -1
- 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
- package/src/takomi-stats.js +5 -5
|
@@ -1,103 +1,208 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
state.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Container, Markdown, Text } from "@earendil-works/pi-tui";
|
|
5
|
+
import { getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import type { ContextManagerState } from "./state";
|
|
8
|
+
import { discoverSkillsFromFilesystem, findSkill, mergeSkills, normalizeName, skillIndexRenderGroups, sortedSkills, type SkillIndexRenderGroup } from "./skill-registry";
|
|
9
|
+
import { persistReportSnapshot, restoreReportFromSession } from "./session-state";
|
|
10
|
+
import { renderCompactCard, renderExpandedMarkdown, renderToolCall, resultText, sanitizePresentation } from "./tool-renderers";
|
|
11
|
+
|
|
12
|
+
function renderSkillIndex(state: ContextManagerState): string {
|
|
13
|
+
const skills = sortedSkills(state.skills);
|
|
14
|
+
if (skills.length === 0) return "Available skills (names only): none discovered.";
|
|
15
|
+
return ["Available skills (names only):", ...skills.map((skill) => `- ${skill.name}`)].join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderManifest(state: ContextManagerState, names: string[]): string {
|
|
19
|
+
if (names.length === 0) return "No skills requested.";
|
|
20
|
+
return names.map((name) => {
|
|
21
|
+
const skill = findSkill(state.skills, name);
|
|
22
|
+
if (!skill) {
|
|
23
|
+
const close = sortedSkills(state.skills).filter((candidate) => normalizeName(candidate.name).includes(normalizeName(name).slice(0, 4))).slice(0, 5).map((candidate) => candidate.name);
|
|
24
|
+
return [`Skill not found: ${name}`, close.length ? `Known close matches: ${close.join(", ")}` : ""].filter(Boolean).join("\n");
|
|
25
|
+
}
|
|
26
|
+
return [`Skill: ${skill.name}`, `Description: ${skill.description ?? "(no description discovered)"}`, `Location: ${skill.location ?? "(no location discovered)"}`].join("\n");
|
|
27
|
+
}).join("\n\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const COMPACT_CATEGORY_LIMIT = 3;
|
|
31
|
+
|
|
32
|
+
function groupSummaryItems(groups: SkillIndexRenderGroup[], maximum = groups.length): string[] {
|
|
33
|
+
const visible = groups.slice(0, maximum).map((group) => `${group.category} ${group.skills.length}`);
|
|
34
|
+
const overflow = groups.length - visible.length;
|
|
35
|
+
return [...visible, ...(overflow > 0 ? [`+${overflow} more categories`] : [])];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function groupSummary(groups: SkillIndexRenderGroup[], maximum = groups.length): string {
|
|
39
|
+
return groupSummaryItems(groups, maximum).join(" · ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderGroupedSkillIndex(groups: SkillIndexRenderGroup[]): string {
|
|
43
|
+
if (groups.length === 0) return "No skills discovered.";
|
|
44
|
+
return groups.map((group) => [
|
|
45
|
+
`## ${group.category}`,
|
|
46
|
+
"",
|
|
47
|
+
...group.skills.map((skill) => `- \`${skill.name}\`${skill.description ? ` — ${skill.description}` : ""}`),
|
|
48
|
+
].join("\n")).join("\n\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function skillMarkdown(text: string): string {
|
|
52
|
+
const separator = text.indexOf("\n\n");
|
|
53
|
+
return separator >= 0 ? text.slice(separator + 2) : text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function loadSkillContent(location: string): Promise<string> {
|
|
57
|
+
const fileName = path.basename(location).toLowerCase();
|
|
58
|
+
if (fileName !== "skill.md" && !location.toLowerCase().endsWith(".md")) throw new Error(`Refusing to load non-markdown skill location: ${location}`);
|
|
59
|
+
return readFile(location, "utf8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function ensureSkillsDiscovered(state: ContextManagerState, cwd: string): Promise<void> {
|
|
63
|
+
if (state.skills.size > 0) return;
|
|
64
|
+
state.skills = mergeSkills(await discoverSkillsFromFilesystem(cwd));
|
|
65
|
+
state.report.skillCount = state.skills.size;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerSkillTools(pi: ExtensionAPI, state: ContextManagerState): void {
|
|
69
|
+
pi.registerTool({
|
|
70
|
+
name: "skill_index",
|
|
71
|
+
label: "Skill Index",
|
|
72
|
+
description: "Return the available skill names only. Use this to inspect capability names without loading descriptions or full instructions.",
|
|
73
|
+
promptSnippet: "List available skill names only for progressive skill loading",
|
|
74
|
+
parameters: Type.Object({}),
|
|
75
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
76
|
+
restoreReportFromSession(state, ctx);
|
|
77
|
+
await ensureSkillsDiscovered(state, ctx.cwd);
|
|
78
|
+
state.report.timestamp = new Date().toISOString();
|
|
79
|
+
state.report.cwd = ctx.cwd;
|
|
80
|
+
state.report.toolCalls.skillIndex += 1;
|
|
81
|
+
persistReportSnapshot(pi, state, "skill_index");
|
|
82
|
+
const groups = skillIndexRenderGroups(state.skills.values());
|
|
83
|
+
return { content: [{ type: "text", text: renderSkillIndex(state) }], details: { skillCount: state.skills.size, groups } };
|
|
84
|
+
},
|
|
85
|
+
renderCall(_args, theme) {
|
|
86
|
+
return renderToolCall("skill_index", undefined, theme);
|
|
87
|
+
},
|
|
88
|
+
renderResult(result, { expanded }, theme) {
|
|
89
|
+
const details = result.details as { skillCount?: number; groups?: SkillIndexRenderGroup[] } | undefined;
|
|
90
|
+
const groups = details?.groups ?? [];
|
|
91
|
+
const count = details?.skillCount ?? groups.reduce((total, group) => total + group.skills.length, 0);
|
|
92
|
+
if (!expanded) {
|
|
93
|
+
return renderCompactCard({
|
|
94
|
+
status: count ? "success" : "pending",
|
|
95
|
+
title: "Skill index",
|
|
96
|
+
summary: count ? `${count} skills across ${groups.length} categories` : "no skills discovered",
|
|
97
|
+
responsiveMetadata: groups.length ? groupSummaryItems(groups, COMPACT_CATEGORY_LIMIT) : undefined,
|
|
98
|
+
}, theme);
|
|
99
|
+
}
|
|
100
|
+
return renderExpandedMarkdown({
|
|
101
|
+
status: count ? "success" : "pending",
|
|
102
|
+
title: "Skill index",
|
|
103
|
+
summary: count ? `${count} skills across ${groups.length} categories` : "no skills discovered",
|
|
104
|
+
metadata: groups.length ? [groupSummary(groups)] : undefined,
|
|
105
|
+
markdown: renderGroupedSkillIndex(groups),
|
|
106
|
+
}, theme);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
pi.registerTool({
|
|
111
|
+
name: "skill_manifest",
|
|
112
|
+
label: "Skill Manifest",
|
|
113
|
+
description: "Return descriptions and locations for selected skills without loading full SKILL.md instructions.",
|
|
114
|
+
promptSnippet: "Show selected skill descriptions and locations without full instructions",
|
|
115
|
+
parameters: Type.Object({ skills: Type.Array(Type.String({ description: "Skill name to inspect" })) }),
|
|
116
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
117
|
+
restoreReportFromSession(state, ctx);
|
|
118
|
+
await ensureSkillsDiscovered(state, ctx.cwd);
|
|
119
|
+
state.report.timestamp = new Date().toISOString();
|
|
120
|
+
state.report.cwd = ctx.cwd;
|
|
121
|
+
state.report.toolCalls.skillManifest += 1;
|
|
122
|
+
persistReportSnapshot(pi, state, "skill_manifest");
|
|
123
|
+
const found = params.skills.filter((name) => Boolean(findSkill(state.skills, name)));
|
|
124
|
+
const missing = params.skills.filter((name) => !findSkill(state.skills, name));
|
|
125
|
+
return { content: [{ type: "text", text: renderManifest(state, params.skills) }], details: { requested: params.skills, found, missing } };
|
|
126
|
+
},
|
|
127
|
+
renderCall(args, theme) {
|
|
128
|
+
return renderToolCall("skill_manifest", `${args.skills.length} requested`, theme);
|
|
129
|
+
},
|
|
130
|
+
renderResult(result, { expanded }, theme) {
|
|
131
|
+
const details = result.details as { requested?: string[]; found?: string[]; missing?: string[] } | undefined;
|
|
132
|
+
const requested = details?.requested ?? [];
|
|
133
|
+
const found = details?.found ?? [];
|
|
134
|
+
const missing = details?.missing ?? [];
|
|
135
|
+
const status = missing.length ? "warning" : requested.length ? "success" : "pending";
|
|
136
|
+
const summary = missing.length ? `${found.length} found · ${missing.length} unavailable` : requested.length ? `${found.length} skills available` : "no skills requested";
|
|
137
|
+
const metadata = missing.length ? `Missing: ${missing.join(", ")}` : `${requested.length} requested`;
|
|
138
|
+
if (!expanded) return renderCompactCard({ status, title: "Skill manifest", summary, metadata }, theme);
|
|
139
|
+
return renderExpandedMarkdown({ status, title: "Skill manifest", summary, metadata: [metadata], markdown: resultText(result) }, theme);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.registerTool({
|
|
144
|
+
name: "skill_load",
|
|
145
|
+
label: "Skill Load",
|
|
146
|
+
description: "Load the full SKILL.md content for one selected skill that will actually be used.",
|
|
147
|
+
promptSnippet: "Load full SKILL.md instructions for one selected skill",
|
|
148
|
+
parameters: Type.Object({ skill: Type.String({ description: "Exact skill name to load" }) }),
|
|
149
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
150
|
+
restoreReportFromSession(state, ctx);
|
|
151
|
+
await ensureSkillsDiscovered(state, ctx.cwd);
|
|
152
|
+
state.report.timestamp = new Date().toISOString();
|
|
153
|
+
state.report.cwd = ctx.cwd;
|
|
154
|
+
state.report.toolCalls.skillLoad += 1;
|
|
155
|
+
const skill = findSkill(state.skills, params.skill);
|
|
156
|
+
if (!skill?.location) {
|
|
157
|
+
persistReportSnapshot(pi, state, "skill_load_not_found");
|
|
158
|
+
return { content: [{ type: "text", text: renderManifest(state, [params.skill]) }], details: { found: false, requested: params.skill }, isError: true };
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const content = await loadSkillContent(skill.location);
|
|
162
|
+
state.report.loadedByTool = [...new Set([...state.report.loadedByTool, skill.name])].sort();
|
|
163
|
+
persistReportSnapshot(pi, state, "skill_load");
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: "text", text: [`Skill: ${skill.name}`, `Location: ${skill.location}`, "", content].join("\n") }],
|
|
166
|
+
details: {
|
|
167
|
+
found: true,
|
|
168
|
+
skill: skill.name,
|
|
169
|
+
description: skill.description,
|
|
170
|
+
location: skill.location,
|
|
171
|
+
lineCount: content.split(/\r?\n/).length,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
+
persistReportSnapshot(pi, state, "skill_load_error");
|
|
177
|
+
return { content: [{ type: "text", text: message }], details: { found: true, skill: skill.name, error: message }, isError: true };
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
renderCall(args, theme) {
|
|
181
|
+
return renderToolCall("skill_load", args.skill, theme);
|
|
182
|
+
},
|
|
183
|
+
renderResult(result, { expanded }, theme) {
|
|
184
|
+
const details = result.details as { found?: boolean; skill?: string; description?: string; location?: string; lineCount?: number; error?: string } | undefined;
|
|
185
|
+
const text = resultText(result);
|
|
186
|
+
if (details?.error || details?.found === false) {
|
|
187
|
+
const summary = details?.error ?? "skill not found";
|
|
188
|
+
if (!expanded) return renderCompactCard({ status: "error", title: "Skill load", summary }, theme);
|
|
189
|
+
return renderExpandedMarkdown({ status: "error", title: "Skill load", summary, markdown: text }, theme);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const name = sanitizePresentation(details?.skill ?? "skill");
|
|
193
|
+
const description = details?.description ? sanitizePresentation(details.description) : undefined;
|
|
194
|
+
const summary = description ?? "skill instructions loaded";
|
|
195
|
+
const metadata = `${details?.lineCount ?? text.split(/\r?\n/).length} lines`;
|
|
196
|
+
if (!expanded) return renderCompactCard({ status: "success", title: name, summary, metadata }, theme);
|
|
197
|
+
|
|
198
|
+
const location = details?.location ? sanitizePresentation(details.location) : undefined;
|
|
199
|
+
const container = new Container();
|
|
200
|
+
container.addChild(new Text(`${theme.fg("success", "✓")} ${theme.fg("accent", theme.bold(name))} ${theme.fg("muted", "skill instructions")}`, 0, 0));
|
|
201
|
+
if (description) container.addChild(new Text(theme.fg("muted", description), 0, 0));
|
|
202
|
+
if (location) container.addChild(new Text(theme.fg("dim", location), 0, 0));
|
|
203
|
+
container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "collapse")), 0, 0));
|
|
204
|
+
container.addChild(new Markdown(sanitizePresentation(skillMarkdown(text)), 0, 1, getMarkdownTheme()));
|
|
205
|
+
return container;
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -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
|
+
}
|