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.
@@ -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: params.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: task.cwd ? path.resolve(rootCwd, task.cwd) : rootCwd,
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: task.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 = params.cwd ? path.resolve(ctx.cwd, params.cwd) : ctx.cwd;
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: params.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
- const rootCwd = params.cwd ? path.resolve(ctx.cwd, params.cwd) : ctx.cwd;
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
- const tasks = resolveTasks(params).map((task) => applyTakomiRoutingDefaults({
209
- ...task,
210
- agent: resolveAgentName(task.agent, byName),
211
- }, routingSnapshot));
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 && !params.overrideUserBlock) {
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
- if (params.confirmProjectAgents !== false && ctx.hasUI && hasProjectAgents(tasks, byName)) {
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 ok = await ctx.ui.confirm("Run project-local Takomi agents?", `Agents: ${[...new Set(names)].join(", ")}\n\nProject agents are repo-controlled. Continue only for trusted repositories.`);
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, agent: tasks[0]!.agent, task: tasks[0]!.task, agentScope }
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 every AI harness(Antigravity, KiloCode, Windsurf, Cursor, Gemini CLI):
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, and they appear in every AI harness you use. No symlinks. No admin privileges. Works on Mac & Windows.
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/antigravity/skills/` | `~/.gemini/antigravity/global_workflows/` |
194
- | **KiloCode** | `~/.kilocode/skills/` | `~/.kilocode/workflows/` |
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
- - **Gemini CLI**: Custom VibeCode skill for large-context processing with Gemini 3 Pro.
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 (landscape vs. portrait), print-ready full-bleed masonry layout generation, and cover art upscaling. Use this skill when asked to: (1) Organize a set of photos into chronological pages, (2) Generate 12" x 30" open spreads (9000 x 3600 px at 300 DPI) with exact gutter margins (150 px vertical center gutter) and custom margins, (3) Create or assemble full-bleed masonry grid layouts, (4) Upscale front, back, or briefcase box covers using high-quality LANCZOS interpolation.
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 relies on four main stages:
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](file:///C:/Users/johno/.gemini/config/skills/photo-book-builder/scripts/organize_photos.py) - Groups and balances photo directories.
92
- * [create_full_bleed_layouts.py](file:///C:/Users/johno/.gemini/config/skills/photo-book-builder/scripts/create_full_bleed_layouts.py) - Assembles masonry grid pages into print-ready spreads.
93
- * [upscale_covers.py](file:///C:/Users/johno/.gemini/config/skills/photo-book-builder/scripts/upscale_covers.py) - General-purpose upscaling script.
94
- * [revert_organization.py](file:///C:/Users/johno/.gemini/config/skills/photo-book-builder/scripts/revert_organization.py) - Restores files back to flat source directories.
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](file:///C:/Users/johno/.gemini/config/skills/photo-book-builder/references/layout_templates.md) - Exact coordinates, dimensions, and grid cells.
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.34",
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": "node scripts/test-skill-selection.js",
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
  },