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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
1
|
+
import { createReadStream, promises as fs } from 'node:fs';
|
|
2
|
+
import readline from 'node:readline';
|
|
2
3
|
import os from 'os';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
|
|
@@ -120,8 +121,14 @@ async function scanPiSessions(root, source, events, sessionRows = [], taskRows =
|
|
|
120
121
|
for (const file of await files(root)) {
|
|
121
122
|
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '', currentTask = null;
|
|
122
123
|
const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0, activityTimestamps: [] };
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
let lines;
|
|
125
|
+
try {
|
|
126
|
+
lines = readline.createInterface({ input: createReadStream(file, { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
127
|
+
} catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
for await (const line of lines) {
|
|
125
132
|
const obj = safeJson(line); if (!obj) continue;
|
|
126
133
|
if (obj.timestamp) { row.start ||= obj.timestamp; row.end = obj.timestamp; addTimestamp(row.activityTimestamps, obj.timestamp); }
|
|
127
134
|
if (obj.type === 'session') { session = obj.id || session; cwd = obj.cwd || cwd; row.key = session; row.session = session; row.cwd = cwd; }
|
|
@@ -164,6 +171,9 @@ async function scanPiSessions(root, source, events, sessionRows = [], taskRows =
|
|
|
164
171
|
}
|
|
165
172
|
const u = msg && msg.usage;
|
|
166
173
|
if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', input: +u.input||0, cache: +u.cacheRead||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0, true) });
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
continue;
|
|
167
177
|
}
|
|
168
178
|
pushTask(taskRows, currentTask);
|
|
169
179
|
row.activeMs = activeDuration(row.activityTimestamps);
|
|
@@ -27,6 +27,8 @@ export type RuntimeHudState = {
|
|
|
27
27
|
activeSessionId?: string;
|
|
28
28
|
launchMode?: string;
|
|
29
29
|
subagentsEnabled?: boolean;
|
|
30
|
+
modeSource?: "idle" | "manual" | "model" | "board";
|
|
31
|
+
modeReason?: string;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
type Tone = "accent" | "warning" | "success" | "error" | "muted" | "dim" | "thinkingMinimal";
|
|
@@ -70,24 +72,30 @@ function badge(theme: Theme, label: string, tone: Tone): string {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export function renderRuntimeStatus(theme: Theme, state: RuntimeHudState): string {
|
|
75
|
+
const source = state.modeSource ?? "idle";
|
|
76
|
+
if (!state.enabled) return [theme.fg("accent", "Takomi"), theme.fg("dim", "off")].join(" ");
|
|
77
|
+
if (source === "idle") return [theme.fg("accent", "Takomi"), theme.fg("dim", "idle")].join(" ");
|
|
78
|
+
|
|
73
79
|
const primary = state.stage ?? state.role;
|
|
74
80
|
const stageBadge = badge(theme, primary, stageTone(state.stage));
|
|
75
|
-
const
|
|
76
|
-
const
|
|
81
|
+
const sourceLabel = source === "manual" ? theme.fg("warning", "manual") : theme.fg("success", source);
|
|
82
|
+
const role = state.stage && state.role !== state.stage ? theme.fg("dim", state.role) : "";
|
|
77
83
|
const plan = state.planMode ? theme.fg("warning", "plan") : theme.fg("dim", "direct");
|
|
78
|
-
const
|
|
79
|
-
|
|
84
|
+
const gate = state.launchMode === "manual" ? theme.fg("warning", "review-gate") : "";
|
|
85
|
+
const subagents = state.subagentsEnabled === false ? theme.fg("error", "subagents:off") : "";
|
|
86
|
+
return [theme.fg("accent", "Takomi"), sourceLabel, stageBadge, role, gate || plan, subagents].filter(Boolean).join(" ");
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
export function renderRuntimeWidget(theme: Theme, state: RuntimeHudState): string[] {
|
|
83
|
-
if (!state.enabled) return [];
|
|
90
|
+
if (!state.enabled || (state.modeSource ?? "idle") === "idle") return [];
|
|
84
91
|
const primary = state.stage ?? state.role;
|
|
92
|
+
const sourceLabel = state.modeSource === "manual" ? theme.fg("warning", "manual") : theme.fg("success", state.modeSource ?? "model");
|
|
85
93
|
const parts = [
|
|
86
94
|
theme.fg("accent", "Takomi"),
|
|
95
|
+
sourceLabel,
|
|
87
96
|
badge(theme, primary, stageTone(state.stage)),
|
|
88
|
-
theme.fg("dim", `role:${state.role}`),
|
|
89
|
-
state.
|
|
90
|
-
state.launchMode === "manual" ? theme.fg("warning", "review-gate") : theme.fg("accent", "auto-gate"),
|
|
97
|
+
state.stage && state.role !== state.stage ? theme.fg("dim", `role:${state.role}`) : "",
|
|
98
|
+
state.launchMode === "manual" ? theme.fg("warning", "review-gate") : "",
|
|
91
99
|
state.planMode ? theme.fg("warning", "plan") : theme.fg("dim", "direct"),
|
|
92
100
|
state.subagentsEnabled === false ? theme.fg("error", "subagents:off") : theme.fg("dim", "subagents:on"),
|
|
93
101
|
state.workflow ? theme.fg("dim", `wf:${state.workflow}`) : "",
|
|
@@ -115,7 +123,7 @@ export class TakomiFooterComponent implements Component {
|
|
|
115
123
|
this.unsubscribeBranchChange();
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
invalidate(): void {}
|
|
126
|
+
invalidate(): void { }
|
|
119
127
|
|
|
120
128
|
render(width: number): string[] {
|
|
121
129
|
const state = this.getState();
|
|
@@ -141,8 +149,7 @@ export class TakomiFooterComponent implements Component {
|
|
|
141
149
|
.filter(([key]) => key !== "takomi-runtime")
|
|
142
150
|
.map(([, value]) => value)
|
|
143
151
|
.filter(Boolean);
|
|
144
|
-
const
|
|
145
|
-
const left = [runtimeStatus, ...extensionStatuses].join(this.theme.fg("dim", " | "));
|
|
152
|
+
const left = extensionStatuses.join(this.theme.fg("dim", " | "));
|
|
146
153
|
const branch = this.footerData.getGitBranch();
|
|
147
154
|
const rightText = [this.ctx.model?.id || "no-model", branch ? `git:${branch}` : ""].filter(Boolean).join(" | ");
|
|
148
155
|
const right = this.theme.fg("dim", rightText);
|
|
@@ -44,10 +44,11 @@ const SubagentParameters = Type.Object({
|
|
|
44
44
|
tasks: Type.Optional(Type.Array(TaskSchema, { description: "Parallel subagent tasks" })),
|
|
45
45
|
confirmLaunch: Type.Optional(Type.Boolean({ description: "Required to launch immediately in manual Takomi launch mode" })),
|
|
46
46
|
previewOnly: Type.Optional(Type.Boolean({ description: "Return the delegation plan without launching" })),
|
|
47
|
+
clarify: Type.Optional(Type.Boolean({ description: "Show the native pi-subagents TUI to preview/edit before execution. Especially useful for chains; requires an interactive Pi TUI." })),
|
|
47
48
|
chain: Type.Optional(Type.Array(TaskSchema, { description: "Sequential chain of subagent tasks" })),
|
|
48
49
|
agentScope: Type.Optional(Type.Union([Type.Literal("user"), Type.Literal("project"), Type.Literal("both")])),
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Project-agent confirmation and hard-stop overrides are enforced server-side;
|
|
51
|
+
// they are intentionally not model-callable parameters.
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
function registerSubagentTool(pi: ExtensionAPI): void {
|
|
@@ -59,9 +60,10 @@ function registerSubagentTool(pi: ExtensionAPI): void {
|
|
|
59
60
|
promptGuidelines: [
|
|
60
61
|
"Use this tool during orchestration when a specialist should handle a task.",
|
|
61
62
|
"Use tasks for independent parallel work and chain for dependent handoffs with {previous}.",
|
|
63
|
+
"Set clarify=true when the user asks to preview/edit a subagent run in the native Pi TUI before launch.",
|
|
62
64
|
"Use model, fallbackModels, and thinking only when deliberate; otherwise let the agent/profile defaults apply.",
|
|
63
65
|
"If review sends work back to the same agent, reuse the same conversationId for continuity.",
|
|
64
|
-
"If a launch is blocked, cancelled, paused, or review-gated, do not retry automatically; wait for the user's next prompt.
|
|
66
|
+
"If a launch is blocked, cancelled, paused, or review-gated, do not retry automatically; wait for the user's next prompt."
|
|
65
67
|
],
|
|
66
68
|
parameters: SubagentParameters,
|
|
67
69
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -30,15 +30,82 @@ export function renderTakomiSubagentCall(params: TakomiSubagentToolParams, theme
|
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (native) return native;
|
|
36
|
-
|
|
37
|
-
const status = (result as any)?.isError ? "failed" : "completed";
|
|
38
|
-
const text = typeof (result as any)?.content === "string"
|
|
33
|
+
function resultText(result: ToolResult): string {
|
|
34
|
+
return typeof (result as any)?.content === "string"
|
|
39
35
|
? (result as any).content
|
|
40
36
|
: Array.isArray((result as any)?.content)
|
|
41
37
|
? (result as any).content.map((part: any) => part?.text ?? "").filter(Boolean).join("\n")
|
|
42
38
|
: JSON.stringify((result as any)?.details ?? {}, null, 2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function summarizeCollapsedResult(text: string, status: string, theme: Theme): string {
|
|
42
|
+
const policyMatch = text.match(/Required policies:\n((?:- .+\n?)+)/);
|
|
43
|
+
const policies = policyMatch?.[1]
|
|
44
|
+
?.split("\n")
|
|
45
|
+
.map((line) => line.replace(/^[-\s]+/, "").trim())
|
|
46
|
+
.filter(Boolean) ?? [];
|
|
47
|
+
const lineCount = text ? text.split(/\r?\n/).length : 0;
|
|
48
|
+
const label = policies.length > 0
|
|
49
|
+
? `policy context loaded: ${policies.join(", ")}`
|
|
50
|
+
: `${lineCount} result line${lineCount === 1 ? "" : "s"} hidden`;
|
|
51
|
+
const icon = status === "failed" ? "⚠" : status === "running" ? "…" : "✓";
|
|
52
|
+
const color = status === "failed" ? "warning" : status === "running" ? "accent" : "success";
|
|
53
|
+
return `${theme.fg(color, `${icon} takomi_subagent ${status}`)}${theme.fg("dim", ` · ${label} (ctrl+o to expand)`)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function recentOutputLines(value: unknown): string[] {
|
|
57
|
+
if (Array.isArray(value)) return value.map(String).filter(Boolean).slice(-3);
|
|
58
|
+
if (typeof value === "string") return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-3);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function livePartialText(result: ToolResult, theme: Theme): string {
|
|
63
|
+
const details = (result as any)?.details ?? {};
|
|
64
|
+
const results = Array.isArray(details.results) ? details.results : [];
|
|
65
|
+
const progress = Array.isArray(details.progress) ? details.progress : [];
|
|
66
|
+
const lines = [
|
|
67
|
+
theme.fg("toolTitle", theme.bold("takomi_subagent running")),
|
|
68
|
+
theme.fg("dim", "Live detail is bounded while streaming so manual scroll/ctrl+o does not jump on every token."),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const rows = results.length ? results : progress;
|
|
72
|
+
if (!rows.length) {
|
|
73
|
+
const text = resultText(result).split(/\r?\n/).find((line) => line.trim())?.trim();
|
|
74
|
+
lines.push(theme.fg("dim", text || "Waiting for subagent progress…"));
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
rows.slice(0, 6).forEach((row: any, index: number) => {
|
|
79
|
+
const status = row.status ?? (row.exitCode === 0 ? "completed" : row.exitCode === -1 ? "running" : row.exitCode === undefined ? "running" : "failed");
|
|
80
|
+
const agent = row.agent ?? `task ${index + 1}`;
|
|
81
|
+
const task = String(row.task ?? "").replace(/\s+/g, " ").trim();
|
|
82
|
+
const currentTool = row.currentTool ?? row.progress?.currentTool;
|
|
83
|
+
const tokens = row.tokens ?? row.progress?.tokens ?? row.usage?.output;
|
|
84
|
+
const tail = recentOutputLines(row.recentOutput ?? row.progress?.recentOutput ?? row.finalOutput ?? row.output);
|
|
85
|
+
lines.push(theme.fg("accent", `${index + 1}. ${agent} ${theme.fg("dim", `[${status}]`)}`));
|
|
86
|
+
if (task) lines.push(theme.fg("dim", ` ${task.slice(0, 140)}${task.length > 140 ? "…" : ""}`));
|
|
87
|
+
if (currentTool || tokens) lines.push(theme.fg("muted", ` ${[currentTool ? `tool:${currentTool}` : "", tokens ? `tokens:${tokens}` : ""].filter(Boolean).join(" | ")}`));
|
|
88
|
+
for (const line of tail) lines.push(theme.fg("dim", ` › ${line.slice(0, 160)}${line.length > 160 ? "…" : ""}`));
|
|
89
|
+
});
|
|
90
|
+
if (rows.length > 6) lines.push(theme.fg("muted", `… ${rows.length - 6} more running item${rows.length - 6 === 1 ? "" : "s"}`));
|
|
91
|
+
lines.push(theme.fg("muted", "Final output will use the normal expandable native subagent renderer."));
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function renderTakomiSubagentResult(result: ToolResult, options: { expanded?: boolean; isPartial?: boolean }, theme: Theme, context: any): any {
|
|
96
|
+
const status = (result as any)?.isError ? "failed" : options.isPartial ? "running" : "completed";
|
|
97
|
+
const text = resultText(result);
|
|
98
|
+
|
|
99
|
+
if (options.isPartial) {
|
|
100
|
+
if (options.expanded) return new Text(livePartialText(result, theme), 0, 0);
|
|
101
|
+
return new Text(summarizeCollapsedResult(text, status, theme), 0, 0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const native = renderNativeSubagentResult(result, options, theme, context);
|
|
105
|
+
if (native) return native;
|
|
106
|
+
|
|
107
|
+
if (!options.expanded) {
|
|
108
|
+
return new Text(summarizeCollapsedResult(text, status, theme), 0, 0);
|
|
109
|
+
}
|
|
43
110
|
return new Text(`${theme.fg("toolTitle", theme.bold(`takomi_subagent ${status}`))}\n${text || "No result content."}`, 0, 0);
|
|
44
111
|
}
|
|
@@ -75,7 +75,7 @@ function resolveTasks(params: TakomiSubagentToolParams): TakomiSubagentToolTask[
|
|
|
75
75
|
fallbackModels: params.fallbackModels,
|
|
76
76
|
thinking: params.thinking,
|
|
77
77
|
conversationId: params.conversationId,
|
|
78
|
-
cwd:
|
|
78
|
+
cwd: undefined,
|
|
79
79
|
checklist: params.checklist,
|
|
80
80
|
}];
|
|
81
81
|
}
|
|
@@ -109,6 +109,26 @@ function modelWithThinking(model: string | undefined, thinking: string | undefin
|
|
|
109
109
|
return `${model}:${level}`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function isPathInside(root: string, candidate: string): boolean {
|
|
113
|
+
const relative = path.relative(root, candidate);
|
|
114
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveRelativeCwd(root: string, value: string | undefined, label: string): string {
|
|
118
|
+
const lexicalRoot = path.resolve(root);
|
|
119
|
+
const lexicalCandidate = value
|
|
120
|
+
? path.isAbsolute(value) ? path.resolve(value) : path.resolve(lexicalRoot, value)
|
|
121
|
+
: lexicalRoot;
|
|
122
|
+
if (!isPathInside(lexicalRoot, lexicalCandidate)) throw new Error(`${label} escapes the current workspace.`);
|
|
123
|
+
|
|
124
|
+
const realRoot = fs.realpathSync(lexicalRoot);
|
|
125
|
+
const realCandidate = fs.realpathSync(lexicalCandidate);
|
|
126
|
+
const stat = fs.statSync(realCandidate);
|
|
127
|
+
if (!stat.isDirectory()) throw new Error(`${label} must be a directory inside the current workspace.`);
|
|
128
|
+
if (!isPathInside(realRoot, realCandidate)) throw new Error(`${label} escapes the current workspace.`);
|
|
129
|
+
return realCandidate;
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
function safeConversationSlug(value: string): string {
|
|
113
133
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96) || "conversation";
|
|
114
134
|
}
|
|
@@ -164,13 +184,14 @@ function agentNameSet(discoverPiAgents: any, cwd: string): Set<string> {
|
|
|
164
184
|
return new Set(discoverUnifiedAgents(discoverPiAgents, cwd, "both").agents.map((agent) => agent.name));
|
|
165
185
|
}
|
|
166
186
|
|
|
167
|
-
function mapSingleTask(task: TakomiSubagentToolTask, names: Set<string
|
|
187
|
+
function mapSingleTask(task: TakomiSubagentToolTask, names: Set<string>, rootCwd: string) {
|
|
168
188
|
const resolvedAgent = resolveAgentName(task.agent, new Map([...names].map((name) => [name, { name } as any])));
|
|
169
189
|
return {
|
|
170
190
|
agent: resolvedAgent,
|
|
171
191
|
task: buildTakomiTaskPrompt({ ...task, agent: resolvedAgent }),
|
|
172
|
-
cwd: task.cwd,
|
|
192
|
+
cwd: resolveRelativeCwd(rootCwd, task.cwd, "task.cwd"),
|
|
173
193
|
model: modelWithThinking(task.model, task.thinking),
|
|
194
|
+
fallbackModels: task.fallbackModels,
|
|
174
195
|
skill: task.skills?.length ? task.skills : undefined,
|
|
175
196
|
};
|
|
176
197
|
}
|
|
@@ -186,20 +207,21 @@ function toSubagentParams(params: TakomiSubagentToolParams, rootCwd: string, dis
|
|
|
186
207
|
cwd: rootCwd,
|
|
187
208
|
context: "fresh" as const,
|
|
188
209
|
async: false,
|
|
189
|
-
clarify:
|
|
210
|
+
clarify: params.clarify === true,
|
|
190
211
|
includeProgress: true,
|
|
191
212
|
sessionDir: stableConversationSessionDir(rootCwd, tasks),
|
|
192
213
|
};
|
|
193
214
|
|
|
194
215
|
if (mode === "single") {
|
|
195
216
|
const task = tasks[0]!;
|
|
196
|
-
const mapped = mapSingleTask(task, names);
|
|
217
|
+
const mapped = mapSingleTask(task, names, rootCwd);
|
|
197
218
|
return {
|
|
198
219
|
...base,
|
|
199
220
|
agent: mapped.agent,
|
|
200
221
|
task: mapped.task,
|
|
201
|
-
cwd:
|
|
222
|
+
cwd: mapped.cwd,
|
|
202
223
|
model: mapped.model,
|
|
224
|
+
fallbackModels: mapped.fallbackModels,
|
|
203
225
|
skill: mapped.skill,
|
|
204
226
|
};
|
|
205
227
|
}
|
|
@@ -207,19 +229,20 @@ function toSubagentParams(params: TakomiSubagentToolParams, rootCwd: string, dis
|
|
|
207
229
|
if (mode === "parallel") {
|
|
208
230
|
return {
|
|
209
231
|
...base,
|
|
210
|
-
tasks: tasks.map((task) => mapSingleTask(task, names)),
|
|
232
|
+
tasks: tasks.map((task) => mapSingleTask(task, names, rootCwd)),
|
|
211
233
|
};
|
|
212
234
|
}
|
|
213
235
|
|
|
214
236
|
return {
|
|
215
237
|
...base,
|
|
216
238
|
chain: tasks.map((task) => {
|
|
217
|
-
const mapped = mapSingleTask(task, names);
|
|
239
|
+
const mapped = mapSingleTask(task, names, rootCwd);
|
|
218
240
|
return {
|
|
219
241
|
agent: mapped.agent,
|
|
220
242
|
task: mapped.task,
|
|
221
|
-
cwd:
|
|
243
|
+
cwd: mapped.cwd,
|
|
222
244
|
model: mapped.model,
|
|
245
|
+
fallbackModels: mapped.fallbackModels,
|
|
223
246
|
skill: mapped.skill,
|
|
224
247
|
};
|
|
225
248
|
}),
|
|
@@ -264,7 +287,7 @@ export function createTakomiPiSubagentsEngine(pi: ExtensionAPI) {
|
|
|
264
287
|
onUpdate: ToolUpdate | undefined,
|
|
265
288
|
ctx: ExtensionContext,
|
|
266
289
|
): Promise<AgentToolResult<Details>> {
|
|
267
|
-
const rootCwd =
|
|
290
|
+
const rootCwd = resolveRelativeCwd(ctx.cwd, params.cwd, "cwd");
|
|
268
291
|
const { executor, discoverPiAgents } = await getExecutor();
|
|
269
292
|
const subagentParams = toSubagentParams(params, rootCwd, discoverPiAgents);
|
|
270
293
|
return executor.execute(id, subagentParams, signal ?? NEVER_ABORT, onUpdate, ctx);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import type { TakomiThinkingLevel } from "../../../src/pi-takomi-core";
|
|
@@ -28,9 +29,8 @@ export type TakomiSubagentToolParams = Partial<TakomiSubagentToolTask> & {
|
|
|
28
29
|
chain?: TakomiSubagentToolTask[];
|
|
29
30
|
confirmLaunch?: boolean;
|
|
30
31
|
previewOnly?: boolean;
|
|
32
|
+
clarify?: boolean;
|
|
31
33
|
agentScope?: TakomiAgentScope;
|
|
32
|
-
confirmProjectAgents?: boolean;
|
|
33
|
-
overrideUserBlock?: boolean;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
type ToolUpdate = (partial: {
|
|
@@ -65,6 +65,10 @@ function hasProjectAgents(tasks: Array<{ agent: string }>, agents: Map<string, T
|
|
|
65
65
|
return tasks.some((task) => agents.get(task.agent)?.source === "project");
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function hostTrustsProjectAgents(): boolean {
|
|
69
|
+
return /^(1|true|yes)$/i.test(process.env.TAKOMI_TRUST_PROJECT_AGENTS || "");
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
function hardStopStore(pi: ExtensionAPI): Map<string, HardStopRecord> {
|
|
69
73
|
const existing = RECENT_HARD_STOPS.get(pi);
|
|
70
74
|
if (existing) return existing;
|
|
@@ -114,6 +118,31 @@ function consumeExpiredHardStop(pi: ExtensionAPI, fingerprint: string): HardStop
|
|
|
114
118
|
return record;
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
function isPathInside(root: string, candidate: string): boolean {
|
|
122
|
+
const relative = path.relative(root, candidate);
|
|
123
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function resolveRelativeCwd(root: string, value: string | undefined, label: string): Promise<string> {
|
|
127
|
+
const lexicalRoot = path.resolve(root);
|
|
128
|
+
const lexicalCandidate = value
|
|
129
|
+
? path.isAbsolute(value) ? path.resolve(value) : path.resolve(lexicalRoot, value)
|
|
130
|
+
: lexicalRoot;
|
|
131
|
+
if (!isPathInside(lexicalRoot, lexicalCandidate)) throw new Error(`${label} escapes the current workspace.`);
|
|
132
|
+
|
|
133
|
+
const [realRoot, realCandidate] = await Promise.all([fs.realpath(lexicalRoot), fs.realpath(lexicalCandidate)]);
|
|
134
|
+
const stat = await fs.stat(realCandidate);
|
|
135
|
+
if (!stat.isDirectory()) throw new Error(`${label} must be a directory inside the current workspace.`);
|
|
136
|
+
if (!isPathInside(realRoot, realCandidate)) throw new Error(`${label} escapes the current workspace.`);
|
|
137
|
+
return realCandidate;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function validateTaskCwds(root: string, tasks: TakomiSubagentToolTask[]): Promise<void> {
|
|
141
|
+
for (const [index, task] of tasks.entries()) {
|
|
142
|
+
await resolveRelativeCwd(root, task.cwd, `tasks[${index}].cwd`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
117
146
|
function getTextContent(result: any): string {
|
|
118
147
|
return (result?.content ?? [])
|
|
119
148
|
.map((item: any) => item?.type === "text" && typeof item.text === "string" ? item.text : "")
|
|
@@ -183,7 +212,7 @@ function resolveTasks(params: TakomiSubagentToolParams): TakomiSubagentToolTask[
|
|
|
183
212
|
fallbackModels: params.fallbackModels,
|
|
184
213
|
thinking: params.thinking,
|
|
185
214
|
conversationId: params.conversationId,
|
|
186
|
-
cwd:
|
|
215
|
+
cwd: undefined,
|
|
187
216
|
checklist: params.checklist,
|
|
188
217
|
}];
|
|
189
218
|
}
|
|
@@ -197,17 +226,31 @@ export async function executeTakomiSubagentTool(
|
|
|
197
226
|
ctx: ExtensionContext,
|
|
198
227
|
) {
|
|
199
228
|
const engine = getEngine(pi);
|
|
200
|
-
|
|
229
|
+
let rootCwd: string;
|
|
230
|
+
try {
|
|
231
|
+
rootCwd = await resolveRelativeCwd(ctx.cwd, params.cwd, "cwd");
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
234
|
+
return textResult(message, { results: [], agentScope: params.agentScope ?? "both" }, true);
|
|
235
|
+
}
|
|
201
236
|
const profile = await loadTakomiProfile(rootCwd);
|
|
202
237
|
const agentScope = params.agentScope ?? "both";
|
|
203
238
|
const agents = discoverTakomiAgents(rootCwd, agentScope);
|
|
204
239
|
const byName = new Map<string, TakomiAgentConfig>(agents.map((agent) => [agent.name, agent]));
|
|
205
240
|
const mode = resolveMode(params);
|
|
206
241
|
const routingSnapshot = await loadTakomiModelRoutingSnapshot(rootCwd);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
242
|
+
let tasks: TakomiSubagentToolTask[];
|
|
243
|
+
try {
|
|
244
|
+
const rawTasks = resolveTasks(params);
|
|
245
|
+
await validateTaskCwds(rootCwd, rawTasks);
|
|
246
|
+
tasks = rawTasks.map((task) => applyTakomiRoutingDefaults({
|
|
247
|
+
...task,
|
|
248
|
+
agent: resolveAgentName(task.agent, byName),
|
|
249
|
+
}, routingSnapshot));
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
252
|
+
return textResult(message, { results: [], availableAgents: agents.map((agent) => agent.name), agentScope }, true);
|
|
253
|
+
}
|
|
211
254
|
|
|
212
255
|
if (!mode) {
|
|
213
256
|
return textResult(
|
|
@@ -222,19 +265,23 @@ export async function executeTakomiSubagentTool(
|
|
|
222
265
|
|
|
223
266
|
const fingerprint = createRunFingerprint(rootCwd, mode, tasks);
|
|
224
267
|
const recentHardStop = consumeExpiredHardStop(pi, fingerprint);
|
|
225
|
-
if (recentHardStop
|
|
268
|
+
if (recentHardStop) {
|
|
226
269
|
return hardStopResult(
|
|
227
270
|
`Subagent launch blocked: the same request was already stopped (${recentHardStop.reason}).\n${recentHardStop.message}`,
|
|
228
271
|
{ results: [], availableAgents: agents.map((agent) => agent.name), agentScope, mode, blockedAt: recentHardStop.at, reason: recentHardStop.reason },
|
|
229
272
|
);
|
|
230
273
|
}
|
|
231
|
-
if (params.overrideUserBlock) {
|
|
232
|
-
hardStopStore(pi).delete(fingerprint);
|
|
233
|
-
}
|
|
234
274
|
|
|
235
|
-
|
|
275
|
+
const projectAgentsTrusted = hostTrustsProjectAgents();
|
|
276
|
+
if (!projectAgentsTrusted && hasProjectAgents(tasks, byName)) {
|
|
236
277
|
const names = tasks.map((task) => byName.get(task.agent)).filter((agent): agent is TakomiAgentConfig => agent?.source === "project").map((agent) => agent.name);
|
|
237
|
-
const
|
|
278
|
+
const uniqueNames = [...new Set(names)].join(", ");
|
|
279
|
+
if (!ctx.hasUI) {
|
|
280
|
+
const message = `Blocked: project-local Takomi agents require interactive approval. Agents: ${uniqueNames}`;
|
|
281
|
+
rememberHardStop(pi, fingerprint, "project-agent-approval-required", message);
|
|
282
|
+
return hardStopResult(message, { results: [], agentScope, mode });
|
|
283
|
+
}
|
|
284
|
+
const ok = await ctx.ui.confirm("Run project-local Takomi agents?", `Agents: ${uniqueNames}\n\nProject agents are repo-controlled. Continue only for trusted repositories.`);
|
|
238
285
|
if (!ok) {
|
|
239
286
|
const message = "Canceled: project-local agents not approved.";
|
|
240
287
|
rememberHardStop(pi, fingerprint, "project-agent-denied", message);
|
|
@@ -269,7 +316,7 @@ export async function executeTakomiSubagentTool(
|
|
|
269
316
|
}
|
|
270
317
|
try {
|
|
271
318
|
const nativeParams: TakomiSubagentToolParams = mode === "single"
|
|
272
|
-
? { ...params,
|
|
319
|
+
? { ...params, ...tasks[0]!, agentScope }
|
|
273
320
|
: mode === "parallel"
|
|
274
321
|
? { ...params, tasks, agentScope }
|
|
275
322
|
: { ...params, chain: tasks, agentScope };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: photo-book-builder
|
|
3
3
|
description: >
|
|
4
|
-
Automates high-resolution photo book creation, including chronological sorting, orientation balancing
|
|
4
|
+
Automates high-resolution photo book creation and correction passes, including chronological sorting, orientation balancing, print-ready full-bleed masonry layout generation, existing layout crop corrections, contact-sheet/cell mapping, and cover art upscaling. Use this skill when asked to: (1) Organize photos into page folders, (2) Generate or regenerate 12" x 30" open spreads (9000 x 3600 px at 300 DPI) with gutter margins, (3) Create or adapt full-bleed masonry grid layouts with project-specific photo counts, (4) Correct existing exports by raising/lowering images or adjusting per-image crop framing without overwriting originals, (5) Upscale front, back, or briefcase box covers using high-quality LANCZOS interpolation.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Photo Book Builder
|
|
@@ -10,7 +10,7 @@ This skill provides a complete system for organizing, structuring, and generatin
|
|
|
10
10
|
|
|
11
11
|
## Core Capabilities
|
|
12
12
|
|
|
13
|
-
The photo book builder
|
|
13
|
+
The photo book builder supports five main stages:
|
|
14
14
|
|
|
15
15
|
```mermaid
|
|
16
16
|
graph TD
|
|
@@ -18,6 +18,8 @@ graph TD
|
|
|
18
18
|
B --> C[Stage 2 & 3: Layout Generation]
|
|
19
19
|
C -->|create_full_bleed_layouts.py| D[9000x3600 px Print Spreads]
|
|
20
20
|
E[Stage 4: Cover Upscaling] -->|upscale_covers.py| F[Print-Ready Cover JPEGs]
|
|
21
|
+
D --> G[Stage 5: Correction Pass]
|
|
22
|
+
G -->|custom renderer or patched generator| H[New Corrected Export Folder]
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
---
|
|
@@ -50,6 +52,8 @@ Layout configurations are determined dynamically based on photo counts and orien
|
|
|
50
52
|
* **Template 6A (6 Photos: 5L, 1P)**: Two landscape rows (4 total), one portrait + landscape col.
|
|
51
53
|
* **Template 6B (6 Photos: 4L, 2P)**: Two large landscape rows (2 total), one double-portrait + double-landscape col.
|
|
52
54
|
|
|
55
|
+
These templates are defaults, not hard limits. When a project already has a custom generator, page folders, or spreads with a different number of photos (for example 23-24 images per spread), preserve that project's generator and layout rules. Do not force the default 5/6-photo half-page templates onto an existing custom book.
|
|
56
|
+
|
|
53
57
|
For a complete coordinate reference and details on column configurations, see [layout_templates.md](references/layout_templates.md).
|
|
54
58
|
|
|
55
59
|
---
|
|
@@ -58,6 +62,8 @@ For a complete coordinate reference and details on column configurations, see [l
|
|
|
58
62
|
The layout compiler reads the page directories, detects photo orientations on the fly, applies the coordinate grids, crops photos to fill cells (using centered LANCZOS fit-scale), and merges them into $9000 \times 3600$ px JPEG spreads.
|
|
59
63
|
* **Page Swapping**: Alternating layout orientation avoids monotonous designs. Left page columns swap if `page_num % 2 == 0`, and right page columns swap if `page_num % 3 == 0`.
|
|
60
64
|
|
|
65
|
+
When regenerating an existing project, first inspect local scripts in the delivery/project folder. If a project-specific script exists, prefer adapting it over the bundled default scripts. Avoid rerunning any organizer/reorganizer that flattens or moves page folders unless the user explicitly asks for a new organization pass.
|
|
66
|
+
|
|
61
67
|
**Execution**:
|
|
62
68
|
Compile the print-ready layouts:
|
|
63
69
|
```bash
|
|
@@ -79,6 +85,44 @@ python scripts/upscale_covers.py --src "path/to/cover.png" --out "path/to/upscal
|
|
|
79
85
|
|
|
80
86
|
---
|
|
81
87
|
|
|
88
|
+
### Stage 5: Existing Layout Correction Pass
|
|
89
|
+
Use this when the user provides screenshots or notes such as "blue means raise the image" and "red means take it down" for already-exported spreads.
|
|
90
|
+
|
|
91
|
+
**Correction workflow**:
|
|
92
|
+
1. Identify the current source workspace, existing export folder, and correction screenshots.
|
|
93
|
+
2. Inspect the local layout generator and `photo_organization_map.json` if present.
|
|
94
|
+
3. Create a contact sheet or map preview that overlays each cell with its source filename and rectangle. This prevents guessing from screenshots.
|
|
95
|
+
4. Translate annotations into per-image corrections:
|
|
96
|
+
* Blue / "raise image up": move the visible subject up in the cell. In crop-ratio terms this usually means increasing vertical crop offset when there is extra source height below the crop.
|
|
97
|
+
* Red / "take it down": move the visible subject down in the cell. In crop-ratio terms this usually means decreasing vertical crop offset when there is extra source height above the crop.
|
|
98
|
+
* If the crop is already pinned at the top or bottom, use a small top-anchored or bottom-anchored overscale instead of adding filler or painting pixels. Start tiny, e.g. `1.006` to `1.012`; larger values can overshoot.
|
|
99
|
+
5. Render to a new export folder. Never overwrite the original export unless the user explicitly asks.
|
|
100
|
+
6. Verify file count, output dimensions, and a reduced review sheet of corrected spreads.
|
|
101
|
+
|
|
102
|
+
**Per-image correction patterns**:
|
|
103
|
+
```python
|
|
104
|
+
CROP_OVERRIDES = {
|
|
105
|
+
"DSC01234.jpg": {"y_ratio": 0.30}, # raise visible subject
|
|
106
|
+
"DSC05678.jpg": {"y_ratio": 0.00}, # lower visible subject
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For edge-pinned images where ratio changes cannot move the frame:
|
|
111
|
+
```python
|
|
112
|
+
TOP_ANCHORED_OVERSCALE = {
|
|
113
|
+
"DSC01234.jpg": 1.010, # tiny downward nudge without filler
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Keep corrections local to filenames/cells. Avoid global crop changes unless every affected image needs the same behavior.
|
|
118
|
+
|
|
119
|
+
**Safety rules for correction passes**:
|
|
120
|
+
* Preserve existing page folders and mapping files.
|
|
121
|
+
* Prefer a renderer that reads existing pages and writes new spreads over a script that reorganizes files.
|
|
122
|
+
* Use output folder names like `10 - layouts - crop-corrections-YYYYMMDD`.
|
|
123
|
+
* Keep a short correction log with source filenames, correction type, output folder, and verification result.
|
|
124
|
+
* On Windows, avoid giant one-shot patches or command lines; keep scripts and path lists small.
|
|
125
|
+
|
|
82
126
|
## Revert Option
|
|
83
127
|
If you need to restart the process or undo the page folder classification, run the revert tool to move photos back to the root workspace directory and remove empty page folders:
|
|
84
128
|
```bash
|
|
@@ -88,9 +132,9 @@ python scripts/revert_organization.py --workspace-dir "path/to/workspace"
|
|
|
88
132
|
## Bundled Resources
|
|
89
133
|
|
|
90
134
|
* **Scripts**:
|
|
91
|
-
* [organize_photos.py](
|
|
92
|
-
* [create_full_bleed_layouts.py](
|
|
93
|
-
* [upscale_covers.py](
|
|
94
|
-
* [revert_organization.py](
|
|
135
|
+
* [organize_photos.py](scripts/organize_photos.py) - Groups and balances photo directories.
|
|
136
|
+
* [create_full_bleed_layouts.py](scripts/create_full_bleed_layouts.py) - Assembles masonry grid pages into print-ready spreads.
|
|
137
|
+
* [upscale_covers.py](scripts/upscale_covers.py) - General-purpose upscaling script.
|
|
138
|
+
* [revert_organization.py](scripts/revert_organization.py) - Restores files back to flat source directories.
|
|
95
139
|
* **References**:
|
|
96
|
-
* [layout_templates.md](
|
|
140
|
+
* [layout_templates.md](references/layout_templates.md) - Exact coordinates, dimensions, and grid cells.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.35",
|
|
4
4
|
"description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
25
|
"postinstall": "node src/postinstall.js",
|
|
26
|
-
"test": "
|
|
26
|
+
"test": "npm run test:typecheck && npm run test:regressions && npm run test:skills",
|
|
27
|
+
"test:typecheck": "tsc --noEmit",
|
|
28
|
+
"test:regressions": "node scripts/test-regressions.js",
|
|
27
29
|
"test:skills": "node scripts/test-skill-selection.js"
|
|
28
30
|
},
|
|
29
31
|
"repository": {
|
|
@@ -52,6 +54,20 @@
|
|
|
52
54
|
"picocolors": "^1.1.1",
|
|
53
55
|
"prompts": "^2.4.2"
|
|
54
56
|
},
|
|
57
|
+
"overrides": {
|
|
58
|
+
"protobufjs": "7.6.3",
|
|
59
|
+
"@protobufjs/utf8": "1.1.1",
|
|
60
|
+
"ws": "8.21.0",
|
|
61
|
+
"brace-expansion": "5.0.6"
|
|
62
|
+
},
|
|
63
|
+
"pnpm": {
|
|
64
|
+
"overrides": {
|
|
65
|
+
"protobufjs": "7.6.3",
|
|
66
|
+
"@protobufjs/utf8": "1.1.1",
|
|
67
|
+
"ws": "8.21.0",
|
|
68
|
+
"brace-expansion": "5.0.6"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
55
71
|
"bugs": {
|
|
56
72
|
"url": "https://github.com/JStaRFilms/VibeCode-Protocol-Suite/issues"
|
|
57
73
|
},
|