takomi 2.1.34 → 2.1.36
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/context-panel.ts +37 -13
- package/.pi/extensions/takomi-runtime/index.ts +68 -1
- 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 +3 -3
- package/.pi/extensions/takomi-subagents/index.ts +10 -4
- package/.pi/extensions/takomi-subagents/native-render.ts +127 -7
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +32 -9
- package/.pi/extensions/takomi-subagents/tool-runner.ts +75 -17
- package/README.md +11 -7
- package/assets/.agent/skills/photo-book-builder/SKILL.md +51 -7
- package/package.json +18 -2
- package/src/cli.js +137 -23
- package/src/harness.js +63 -71
- package/src/owned-tree.js +114 -0
- package/src/pi-harness.js +53 -24
- package/src/pi-installer.js +12 -36
- package/src/store.js +5 -40
- package/src/takomi-stats.js +14 -3
- package/src/utils.js +81 -44
|
@@ -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
|
}
|
|
@@ -193,13 +214,14 @@ function toSubagentParams(params: TakomiSubagentToolParams, rootCwd: string, dis
|
|
|
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,6 +1,7 @@
|
|
|
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
|
-
import type { TakomiThinkingLevel } from "../../../src/pi-takomi-core";
|
|
4
|
+
import type { TakomiLaunchMode, TakomiThinkingLevel } from "../../../src/pi-takomi-core";
|
|
4
5
|
import { loadTakomiProfile } from "../takomi-runtime/profile";
|
|
5
6
|
import { applyTakomiRoutingDefaults, loadTakomiModelRoutingSnapshot } from "../takomi-runtime/model-routing-defaults";
|
|
6
7
|
import { resolveAgentName } from "./agent-aliases";
|
|
@@ -30,8 +31,6 @@ export type TakomiSubagentToolParams = Partial<TakomiSubagentToolTask> & {
|
|
|
30
31
|
previewOnly?: boolean;
|
|
31
32
|
clarify?: boolean;
|
|
32
33
|
agentScope?: TakomiAgentScope;
|
|
33
|
-
confirmProjectAgents?: boolean;
|
|
34
|
-
overrideUserBlock?: boolean;
|
|
35
34
|
};
|
|
36
35
|
|
|
37
36
|
type ToolUpdate = (partial: {
|
|
@@ -66,6 +65,10 @@ function hasProjectAgents(tasks: Array<{ agent: string }>, agents: Map<string, T
|
|
|
66
65
|
return tasks.some((task) => agents.get(task.agent)?.source === "project");
|
|
67
66
|
}
|
|
68
67
|
|
|
68
|
+
function hostTrustsProjectAgents(): boolean {
|
|
69
|
+
return /^(1|true|yes)$/i.test(process.env.TAKOMI_TRUST_PROJECT_AGENTS || "");
|
|
70
|
+
}
|
|
71
|
+
|
|
69
72
|
function hardStopStore(pi: ExtensionAPI): Map<string, HardStopRecord> {
|
|
70
73
|
const existing = RECENT_HARD_STOPS.get(pi);
|
|
71
74
|
if (existing) return existing;
|
|
@@ -115,6 +118,31 @@ function consumeExpiredHardStop(pi: ExtensionAPI, fingerprint: string): HardStop
|
|
|
115
118
|
return record;
|
|
116
119
|
}
|
|
117
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
|
+
|
|
118
146
|
function getTextContent(result: any): string {
|
|
119
147
|
return (result?.content ?? [])
|
|
120
148
|
.map((item: any) => item?.type === "text" && typeof item.text === "string" ? item.text : "")
|
|
@@ -164,6 +192,17 @@ function withNativeHardStop(result: any, hardStop: { reason: string; message: st
|
|
|
164
192
|
},
|
|
165
193
|
};
|
|
166
194
|
}
|
|
195
|
+
|
|
196
|
+
function readRuntimeLaunchMode(ctx: ExtensionContext): TakomiLaunchMode | undefined {
|
|
197
|
+
const entries = ctx.sessionManager.getEntries();
|
|
198
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
199
|
+
const entry = entries[i] as { type?: string; customType?: string; data?: { launchMode?: unknown } };
|
|
200
|
+
if (entry.type !== "custom" || entry.customType !== "takomi-runtime-state") continue;
|
|
201
|
+
if (entry.data?.launchMode === "manual" || entry.data?.launchMode === "auto") return entry.data.launchMode;
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
167
206
|
function resolveMode(params: TakomiSubagentToolParams): "single" | "parallel" | "chain" | undefined {
|
|
168
207
|
const hasChain = Boolean(params.chain?.length);
|
|
169
208
|
const hasParallel = Boolean(params.tasks?.length);
|
|
@@ -184,7 +223,7 @@ function resolveTasks(params: TakomiSubagentToolParams): TakomiSubagentToolTask[
|
|
|
184
223
|
fallbackModels: params.fallbackModels,
|
|
185
224
|
thinking: params.thinking,
|
|
186
225
|
conversationId: params.conversationId,
|
|
187
|
-
cwd:
|
|
226
|
+
cwd: undefined,
|
|
188
227
|
checklist: params.checklist,
|
|
189
228
|
}];
|
|
190
229
|
}
|
|
@@ -198,17 +237,32 @@ export async function executeTakomiSubagentTool(
|
|
|
198
237
|
ctx: ExtensionContext,
|
|
199
238
|
) {
|
|
200
239
|
const engine = getEngine(pi);
|
|
201
|
-
|
|
240
|
+
let rootCwd: string;
|
|
241
|
+
try {
|
|
242
|
+
rootCwd = await resolveRelativeCwd(ctx.cwd, params.cwd, "cwd");
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
245
|
+
return textResult(message, { results: [], agentScope: params.agentScope ?? "both" }, true);
|
|
246
|
+
}
|
|
202
247
|
const profile = await loadTakomiProfile(rootCwd);
|
|
248
|
+
const runtimeLaunchMode = readRuntimeLaunchMode(ctx);
|
|
203
249
|
const agentScope = params.agentScope ?? "both";
|
|
204
250
|
const agents = discoverTakomiAgents(rootCwd, agentScope);
|
|
205
251
|
const byName = new Map<string, TakomiAgentConfig>(agents.map((agent) => [agent.name, agent]));
|
|
206
252
|
const mode = resolveMode(params);
|
|
207
253
|
const routingSnapshot = await loadTakomiModelRoutingSnapshot(rootCwd);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
254
|
+
let tasks: TakomiSubagentToolTask[];
|
|
255
|
+
try {
|
|
256
|
+
const rawTasks = resolveTasks(params);
|
|
257
|
+
await validateTaskCwds(rootCwd, rawTasks);
|
|
258
|
+
tasks = rawTasks.map((task) => applyTakomiRoutingDefaults({
|
|
259
|
+
...task,
|
|
260
|
+
agent: resolveAgentName(task.agent, byName),
|
|
261
|
+
}, routingSnapshot));
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
264
|
+
return textResult(message, { results: [], availableAgents: agents.map((agent) => agent.name), agentScope }, true);
|
|
265
|
+
}
|
|
212
266
|
|
|
213
267
|
if (!mode) {
|
|
214
268
|
return textResult(
|
|
@@ -223,19 +277,23 @@ export async function executeTakomiSubagentTool(
|
|
|
223
277
|
|
|
224
278
|
const fingerprint = createRunFingerprint(rootCwd, mode, tasks);
|
|
225
279
|
const recentHardStop = consumeExpiredHardStop(pi, fingerprint);
|
|
226
|
-
if (recentHardStop
|
|
280
|
+
if (recentHardStop) {
|
|
227
281
|
return hardStopResult(
|
|
228
282
|
`Subagent launch blocked: the same request was already stopped (${recentHardStop.reason}).\n${recentHardStop.message}`,
|
|
229
283
|
{ results: [], availableAgents: agents.map((agent) => agent.name), agentScope, mode, blockedAt: recentHardStop.at, reason: recentHardStop.reason },
|
|
230
284
|
);
|
|
231
285
|
}
|
|
232
|
-
if (params.overrideUserBlock) {
|
|
233
|
-
hardStopStore(pi).delete(fingerprint);
|
|
234
|
-
}
|
|
235
286
|
|
|
236
|
-
|
|
287
|
+
const projectAgentsTrusted = hostTrustsProjectAgents();
|
|
288
|
+
if (!projectAgentsTrusted && hasProjectAgents(tasks, byName)) {
|
|
237
289
|
const names = tasks.map((task) => byName.get(task.agent)).filter((agent): agent is TakomiAgentConfig => agent?.source === "project").map((agent) => agent.name);
|
|
238
|
-
const
|
|
290
|
+
const uniqueNames = [...new Set(names)].join(", ");
|
|
291
|
+
if (!ctx.hasUI) {
|
|
292
|
+
const message = `Blocked: project-local Takomi agents require interactive approval. Agents: ${uniqueNames}`;
|
|
293
|
+
rememberHardStop(pi, fingerprint, "project-agent-approval-required", message);
|
|
294
|
+
return hardStopResult(message, { results: [], agentScope, mode });
|
|
295
|
+
}
|
|
296
|
+
const ok = await ctx.ui.confirm("Run project-local Takomi agents?", `Agents: ${uniqueNames}\n\nProject agents are repo-controlled. Continue only for trusted repositories.`);
|
|
239
297
|
if (!ok) {
|
|
240
298
|
const message = "Canceled: project-local agents not approved.";
|
|
241
299
|
rememberHardStop(pi, fingerprint, "project-agent-denied", message);
|
|
@@ -244,7 +302,7 @@ export async function executeTakomiSubagentTool(
|
|
|
244
302
|
}
|
|
245
303
|
const plan = createTakomiDelegationPlan({
|
|
246
304
|
source: "takomi-tool",
|
|
247
|
-
launchMode: profile.launchMode ?? "auto",
|
|
305
|
+
launchMode: runtimeLaunchMode ?? profile.launchMode ?? "auto",
|
|
248
306
|
profile,
|
|
249
307
|
tasks: tasks.map((task, index) => ({
|
|
250
308
|
id: task.conversationId ?? `direct-${index + 1}`,
|
|
@@ -270,7 +328,7 @@ export async function executeTakomiSubagentTool(
|
|
|
270
328
|
}
|
|
271
329
|
try {
|
|
272
330
|
const nativeParams: TakomiSubagentToolParams = mode === "single"
|
|
273
|
-
? { ...params,
|
|
331
|
+
? { ...params, ...tasks[0]!, agentScope }
|
|
274
332
|
: mode === "parallel"
|
|
275
333
|
? { ...params, tasks, agentScope }
|
|
276
334
|
: { ...params, chain: tasks, agentScope };
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Takomi now ships a Pi-native `takomi-context-manager` extension. It reduces prom
|
|
|
63
63
|
|
|
64
64
|
### Option A: Global Install (Best for Multi-IDE Users) ⭐
|
|
65
65
|
|
|
66
|
-
Install once. Use everywhere. Your skills follow you across
|
|
66
|
+
Install once. Use everywhere. Your skills follow you across detected AI harnesses (Antigravity, Claude Code, Codex, Cursor, Kilo Code, Pi/shared Agent Skills, Windsurf):
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
69
|
# Using pnpm
|
|
@@ -184,17 +184,21 @@ npx -y skills add https://github.com/JStaRFilms/VibeCode-Protocol-Suite --skill
|
|
|
184
184
|
|
|
185
185
|
**One install. Every IDE. Zero friction.**
|
|
186
186
|
|
|
187
|
-
Takomi v2.0 introduces the **Global Skills Router** — install skills once
|
|
187
|
+
Takomi v2.0 introduces the **Global Skills Router** — install skills once into `~/.takomi/`, and Takomi syncs them into each harness' current global skills directory. You can choose symlink/junction mode for one canonical copy, auto fallback, or plain copy mode. Works on Mac & Windows.
|
|
188
|
+
|
|
189
|
+
Note: `SKILL.md` is portable, but `~/.agents/skills` is **not** a universal global skills directory. Pi does load `~/.agents/skills`, and Takomi keeps that as the Pi/shared Agent Skills target. Other harnesses use their own global paths. See `docs/skills-harness-targets.md` for the current checked path table.
|
|
188
190
|
|
|
189
191
|
### Supported Harnesses
|
|
190
192
|
|
|
191
193
|
| Harness | Global Skills Path | Global Workflows Path |
|
|
192
194
|
|---|---|---|
|
|
193
|
-
| **Antigravity** | `~/.gemini/
|
|
194
|
-
| **
|
|
195
|
+
| **Antigravity** | `~/.gemini/config/skills/` | `~/.gemini/config/global_workflows/` |
|
|
196
|
+
| **Claude Code** | `~/.claude/skills/` | _(skills only)_ |
|
|
197
|
+
| **Codex** | `~/.codex/skills/` | _(skills only)_ |
|
|
198
|
+
| **Cursor** | `~/.cursor/skills/` | _(skills only)_ |
|
|
199
|
+
| **Kilo Code** | `~/.kilocode/skills/` | `~/.kilocode/workflows/` |
|
|
200
|
+
| **Pi / shared Agent Skills** | `~/.agents/skills/` | _(skills only)_ |
|
|
195
201
|
| **Windsurf** | `~/.codeium/windsurf/skills/` | `~/.codeium/windsurf/global_workflows/` |
|
|
196
|
-
| **Global agents-compatible CLIs** _(e.g., Codex, Gemini CLI)_ | `~/.agents/skills/` | _(skills only)_ |
|
|
197
|
-
| **Cursor** | `~/.cursor/skills/` | _(uses rules)_ |
|
|
198
202
|
|
|
199
203
|
### CLI Commands
|
|
200
204
|
|
|
@@ -505,7 +509,7 @@ Externally sourced skills and optional Pi packages retain credit to their upstre
|
|
|
505
509
|
- **Context7**: From [upstash/context7](https://github.com/upstash/context7) — fresh library documentation fetcher.
|
|
506
510
|
- **Audit Website**: From [squirrelscan/skills](https://github.com/squirrelscan/skills) — professional website auditor.
|
|
507
511
|
- **Convex Skills**: From [waynesutton/convexskills](https://github.com/waynesutton/convexskills) — complete Convex development suite including **Functions**, **Schema Validation**, **Realtime**, **Agents**, **File Storage**, **Migrations**, **HTTP Actions**, **Cron Jobs**, **Component Authoring**, **Best Practices**, **Security Audit**, **Security Check**, **Avoid Feature Creep**, and **Optimize Agent Context**.
|
|
508
|
-
- **
|
|
512
|
+
- **Anti-Gravity**: Custom skill for running Google's current Anti-Gravity CLI workflow for large-context review and analysis.
|
|
509
513
|
- **Google Stitch Skills**: From [google-labs-code/stitch-skills](https://github.com/google-labs-code/stitch-skills) — Design-to-code suite including **design-md**, **enhance-prompt**, **stitch-loop**, **react-components**, and **shadcn-ui**.
|
|
510
514
|
- **Jules**: From [sanjay3290/ai-skills](https://github.com/sanjay3290/ai-skills) — delegate coding tasks to Google Jules AI agent.
|
|
511
515
|
- **Subagent Execution**: Built on **[`pi-subagents`](https://github.com/nicobailon/pi-subagents)** by **Nico Bailon** — providing the underlying Pi extension for delegated subagent runs (result rendering, live progress, single/parallel/chain execution, session/artifact handling, and related subagent tooling), upon which Takomi adds its own lifecycle orchestration, model-routing policy, and workflow metadata.
|
|
@@ -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.36",
|
|
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
|
},
|