@a2f0/agent-tool 0.1.11 → 0.1.13

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/README.md CHANGED
@@ -11,6 +11,9 @@ version management, and portable skills for Claude Code, Codex, and OpenCode.
11
11
  `bun.lock` in sync.
12
12
  - **Share skills**: install the same workflow instructions where each harness
13
13
  discovers them, and check them in CI.
14
+ - **Render brand images**: build app icons, splash screens, favicons, and store
15
+ graphics from the SVGs in `assets/brand/`, with every size and destination
16
+ declared in `agent-tool.json`.
14
17
  - **Compact output**: run noisy commands with full private logs and bounded
15
18
  diagnostics, keeping routine output out of an agent's context.
16
19
 
@@ -109,6 +112,8 @@ agent-tool skills install --apply
109
112
  | `skills list` | List embedded portable skills |
110
113
  | `skills install [--harness all\|claude\|codex\|opencode] [--apply]` | Preview by default; install or update managed skills |
111
114
  | `skills check [--harness all\|claude\|codex\|opencode]` | Read-only CI check for current managed skills |
115
+ | `images list [target...]` | List the files each image target writes, without rendering |
116
+ | `images build [target...] [--out dir]` | Render image targets from `assets/brand/*.svg`; `--out` redirects one target |
112
117
  | `init` / `config show` | Create or inspect data-only project policy |
113
118
  | `doctor` | Inspect local versions and required CLI flags without running a model |
114
119
 
@@ -256,6 +261,38 @@ the commit restores the manifests, lockfile, and index, unless HEAD or other
256
261
  paths changed, in which case it leaves that state for inspection. Repeating
257
262
  against the same base creates no commit.
258
263
 
264
+ ### Images
265
+
266
+ `images` declares named targets of brand images rendered from SVG sources in
267
+ `assets/brand/`. Each output names its file, pixel size or variants (such as
268
+ Android densities or `@2x`/`@3x`), and the layers composed into it:
269
+
270
+ ```json
271
+ {
272
+ "schemaVersion": 1,
273
+ "images": {
274
+ "targets": {
275
+ "ios": {
276
+ "dir": "ios/App/Assets.xcassets",
277
+ "outputs": [
278
+ {
279
+ "path": "AppIcon.appiconset/AppIcon.png",
280
+ "size": 1024,
281
+ "opaque": true,
282
+ "layers": [{ "source": "background", "fit": "cover" }, { "source": "logo", "scale": 0.7 }]
283
+ }
284
+ ]
285
+ }
286
+ }
287
+ }
288
+ }
289
+ ```
290
+
291
+ `agent-tool images build ios` writes PNG, ICO, SVG, and Android color resource
292
+ outputs with a bundled WebAssembly renderer, so builds need no ImageMagick or
293
+ librsvg and render identically on every machine. See the
294
+ [image reference](docs/images.md) for every setting.
295
+
259
296
  ## Portable skills
260
297
 
261
298
  Canonical skills live in `skills/`. The build embeds them, and the installer
@@ -294,52 +331,10 @@ anything. Use the same `--harness` for install and check.
294
331
 
295
332
  ### Dependency upgrades
296
333
 
297
- Invoke `update-dependencies` in Claude Code, Codex, or OpenCode to inventory and
298
- upgrade package dependencies, runtimes, mise tools, Actions, native toolchains,
299
- Ansible, Terraform providers/modules, and other repository-owned pins. It reads
300
- official migration guides, upgrades coupled dependencies together, resolves
301
- deprecations, and records validated, constrained, and skipped upgrades. It follows
302
- the repository's existing upgrade skills and support policy.
303
-
304
- Read-only advisory checks cover baseline and candidate lockfiles, including
305
- resolved transitive and native dependencies. The skill traces findings to their
306
- owning dependency and prefers an upstream-supported fix. A scoped security patch
307
- override requires compatibility evidence, a regression check of the affected
308
- integration, and a documented removal condition; blind audit fixes and blanket
309
- compatibility overrides are excluded. Remaining advisories and audit limits are
310
- reported explicitly. For published packages, the skill also checks tarballs and
311
- consumer installs with their documented supported managers, including override
312
- syntax and runtime requirements.
313
-
314
- The skill requires a dry run before any infrastructure mutation, including ones
315
- triggered by hooks or CI on push/merge. Resource destruction or replacement, and
316
- previews that cannot establish safety, skip the affected upgrade group. A Wrangler
317
- bundle dry run alone does not prove remote resource safety. The skill does not
318
- itself authorize deployment or shipping; use `ship-pr` when requested.
319
- For Wrangler, the skill treats remote D1 migration listing as potentially
320
- mutating and holds deployment when a migration is pending or the upload cannot
321
- be proven to match the validated preview artifacts.
322
-
323
- For a full refreshed Terraform saved plan, export its private JSON and screen it:
324
-
325
- ```sh
326
- terraform show -json /private/path/upgrade.tfplan > /private/path/upgrade.json
327
- agent-tool dependencies check-terraform-plan /private/path/upgrade.json
328
- ```
329
-
330
- The check returns JSON with `ok`, `issues`, and `resourceActions`, and exits 1 for
331
- unsafe or unsupported plans. It rejects delete and replacement actions, unknown
332
- actions, destructive drift, incomplete/deferred plans, unpassed checks, and opaque
333
- provider action invocations. It requires JSON format 1.x and `complete: true`
334
- (available from Terraform 1.8); older plans without completeness evidence fail.
335
- OpenTofu's current JSON format omits `complete` and is not accepted; report that
336
- limitation rather than fabricating the field.
337
- Missing `resource_changes` is valid for a complete empty or output-only plan;
338
- removing an output is not a resource deletion. Invalid JSON fails without echoing
339
- the input. The command never applies or contacts a backend and prints no attribute
340
- values. It cannot establish plan freshness, environment/identity, provider or
341
- provisioner side effects, or authorization; inspect those before deploying and
342
- apply the exact approved saved plan. Keep state and plans out of Git.
334
+ Invoke `update-dependencies` to upgrade repository-owned dependencies, runtimes,
335
+ and tool pins with compatibility checks and safe infrastructure previews.
336
+ See the [dependency upgrade guide](docs/dependency-upgrades.md) for advisory
337
+ checks, preview limits, the Terraform plan guard, and maintaining upgrade records.
343
338
 
344
339
  ## Review guarantees and limits
345
340
 
@@ -435,6 +430,7 @@ yet.
435
430
  ## Further reading
436
431
 
437
432
  - [Design](docs/design.md): component boundaries and implementation choices.
433
+ - [Images](docs/images.md): brand image sources, targets, and output settings.
438
434
  - [Validation record](docs/validation.md): observed test results and their
439
435
  limits.
440
436
  - [Extraction record](docs/extraction.md): the projects this tool was extracted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a2f0/agent-tool",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "Portable cross-agent review, PR workflows, and shared skills",
6
6
  "license": "UNLICENSED",
@@ -33,5 +33,8 @@
33
33
  "devDependencies": {
34
34
  "@types/bun": "1.4.2",
35
35
  "typescript": "7.0.2"
36
+ },
37
+ "dependencies": {
38
+ "@resvg/resvg-wasm": "2.6.2"
36
39
  }
37
40
  }
@@ -6,8 +6,9 @@ description: Update a repository's dependency manifests, lockfiles, runtimes, to
6
6
  # Update Dependencies
7
7
 
8
8
  Use the repository's package managers and installed `agent-tool`. Read `AGENTS.md`,
9
- README, linked setup/release documents, and repository-owned upgrade skills before
10
- editing. Follow their hooks, version policy, checks, and requested stopping point.
9
+ README, existing dependency-maintenance notes, linked setup/release documents,
10
+ and repository-owned upgrade skills before editing. Follow their hooks, version
11
+ policy, checks, and requested stopping point.
11
12
  Update applicable dependencies throughout the repository, including workspaces
12
13
  and examples. This skill does not authorize deploying, merging, publishing, or
13
14
  changing global tools; use existing user authorization and `ship-pr` when shipping
@@ -139,7 +140,8 @@ Source: [Ansible check mode limits](https://docs.ansible.com/projects/ansible/la
139
140
  ## Inventory and choose compatible targets
140
141
 
141
142
  Record each dependency source, current resolved version, latest stable candidate,
142
- selected target, compatibility constraints, upstream migration links, and checks.
143
+ selected target, compatibility constraints, upstream migration links, and checks
144
+ in the run ledger described under Report.
143
145
  Query authoritative registries, official releases, support matrices, and migration
144
146
  guides at execution time; do not treat remembered versions or prereleases as
145
147
  latest stable. Inspect runtime requirements, peer dependencies, ABI/API changes,
@@ -246,11 +248,33 @@ changed HEAD. Check deployment safety again before its push/open/merge operation
246
248
 
247
249
  ## Report
248
250
 
249
- Keep a compact ledger of old/selected versions, completed migrations, validation
250
- and warning results, preview environment/commit/tool versions and safe action
251
- summary, and every skipped/constrained dependency with the reason. Include baseline
252
- and final audit results, security overrides with their rationale and removal
253
- condition, unresolved advisories, and audit coverage limits. For shipping,
254
- include branch, reviewed commit/verdict, PR, merge, publish/deploy result, and final
255
- checkout state. For a repository sweep, keep one entry per repository, including
256
- unready or unchanged checkouts. Do not publish credentials, state, or plan contents.
251
+ Keep run-specific evidence in the upgrade PR body or a linked PR comment: the
252
+ inventory and old/selected versions, completed migrations, validation and warnings,
253
+ baseline/final audits and coverage limits, preview environment/commit/tool versions
254
+ and safe action summary, and skipped/constrained dependencies with reasons. Include
255
+ security overrides, their rationale and removal condition, and unresolved advisories.
256
+ If no PR is requested, provide this ledger in the task report; reporting does not
257
+ authorize opening a PR or issue. For a sweep, keep one entry per repository,
258
+ including unready and unchanged checkouts, and use an existing tracking issue only
259
+ when explicitly requested. Never create an issue merely to store upgrade evidence.
260
+
261
+ Keep durable repository guidance focused on what the next upgrade needs: current
262
+ compatibility constraints, intentional pins/overrides, unresolved advisories, and
263
+ reusable repository-specific procedures. Prefer the existing maintenance document;
264
+ otherwise create `docs/dependency-upgrades.md` (or the repository's equivalent)
265
+ only when there is substantive guidance to preserve. For each constraint, record
266
+ its rationale, upstream advisory/migration/issue link or originating PR, and the
267
+ condition and check for removing or revisiting it. Do not copy version inventories,
268
+ audit output, validation/preview receipts, or dated run journals into that document.
269
+
270
+ On every upgrade, reconcile the existing maintenance document against the final
271
+ dependency graph and checks: update constraints in place, remove resolved entries,
272
+ and link the PR for historical evidence. Keep the root README focused on purpose,
273
+ setup, supported requirements, and normal usage, with a concise maintenance link
274
+ when needed. Keep dependency-maintenance guidance in `AGENTS.md` to a short
275
+ routing rule and link; do not accumulate upgrade reports in either entrypoint
276
+ or duplicate the maintenance document.
277
+
278
+ For shipping, report the branch, reviewed commit/verdict, PR, merge, publish/deploy
279
+ result, and final checkout state. Keep full logs private; never publish credentials,
280
+ state, or plan contents in any of these locations.
package/src/cli.ts CHANGED
@@ -32,6 +32,8 @@ Usage: agent-tool [--repo <directory>] [--config <file>] <command>
32
32
  skills list
33
33
  skills install [--harness <all|claude|codex|opencode>] [--apply]
34
34
  skills check [--harness <all|claude|codex|opencode>]
35
+ images list [target...] JSON files each image target writes
36
+ images build [target...] [--out <dir>] Render assets/brand/*.svg per agent-tool.json
35
37
  init Create agent-tool.json without overwriting
36
38
  config show
37
39
  doctor JSON versions and supported review flags
@@ -85,6 +87,22 @@ export function main(argv = process.argv.slice(2)): number | Promise<number> {
85
87
  const result = doctor(process.cwd()); output(result);
86
88
  return result.tools.find(item => item.name === "git")?.compatible && result.tools.some(item => ["claude", "codex", "opencode"].includes(item.name) && item.compatible) ? 0 : 1;
87
89
  }
90
+ if (command === "images") {
91
+ const action = args.shift();
92
+ usage(action === "list" || action === "build", "Usage: agent-tool images <list|build> [target...] [--out <dir>]");
93
+ const names: string[] = [];
94
+ let out: string | undefined;
95
+ while (args.length) {
96
+ const value = args.shift()!;
97
+ if (value === "--out") { usage(action === "build" && out === undefined && !!args[0] && !args[0].startsWith("--"), "--out requires one directory and applies to build."); out = args.shift(); }
98
+ else { usage(!value.startsWith("-"), `Unknown images argument '${value}'.`); names.push(value); }
99
+ }
100
+ const { images } = loadConfig();
101
+ // Loaded on demand so other commands never initialize the renderer.
102
+ return import("./images/build").then(async ({ buildImages, listImages }) => {
103
+ output(action === "list" ? listImages(images, names) : await buildImages(process.cwd(), images, names, out)); return 0;
104
+ });
105
+ }
88
106
  if (command === "skills") {
89
107
  const action = args.shift();
90
108
  if (action === "list") { usage(args.length === 0, "Usage: agent-tool skills list"); output(Object.keys(BUNDLED_SKILLS)); return 0; }
package/src/config.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, lstatSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { parseImages, type ImagesConfig } from "./images/config";
3
4
 
4
5
  export interface RequiredCheck {
5
6
  name: string;
@@ -13,6 +14,7 @@ export interface AgentToolConfig {
13
14
  review: { claudeModel: string | null; codexModel: string | null; opencodeModel: string; opencodeVariants: Record<string, string>; timeoutMs: number };
14
15
  versions: { packages: string[] | null; bundles: string[]; lockfile: "bun.lock" | null; validate: string[][]; commitMessage: string };
15
16
  pr: { rejectClaudeBranding: boolean };
17
+ images: ImagesConfig;
16
18
  }
17
19
 
18
20
  export const DEFAULT_CONFIG: AgentToolConfig = {
@@ -26,6 +28,7 @@ export const DEFAULT_CONFIG: AgentToolConfig = {
26
28
  review: { claudeModel: null, codexModel: null, opencodeModel: "deepseek/deepseek-v4-pro", opencodeVariants: { low: "low", medium: "medium", high: "high", xhigh: "max", max: "max" }, timeoutMs: 600_000 },
27
29
  versions: { packages: null, bundles: [], lockfile: "bun.lock", validate: [], commitMessage: "chore: bump package versions" },
28
30
  pr: { rejectClaudeBranding: false },
31
+ images: { targets: {} },
29
32
  };
30
33
 
31
34
  function object(value: unknown, name: string): Record<string, unknown> {
@@ -57,6 +60,7 @@ export function parseConfig(source: string): AgentToolConfig {
57
60
  keys(settings, Object.keys(result[section]), section);
58
61
  Object.assign(result[section], settings);
59
62
  }
63
+ if (input.images !== undefined) result.images = parseImages(input.images);
60
64
  const invalid = (setting: string): never => { throw new Error(`Invalid config setting '${setting}'.`); };
61
65
  if (typeof result.subject.conventional !== "boolean") invalid("subject.conventional");
62
66
  if (!Number.isSafeInteger(result.subject.maxLength) || result.subject.maxLength < 1) invalid("subject.maxLength");
@@ -0,0 +1,160 @@
1
+ import { initWasm, Resvg } from "@resvg/resvg-wasm";
2
+ import wasm from "@resvg/resvg-wasm/index_bg.wasm" with { type: "file" };
3
+ import { randomUUID } from "node:crypto";
4
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { composeSvg, type Canvas } from "./compose";
7
+ import { expand, format, SOURCE_DIRECTORY, verbatim, type ImageOutput, type ImagesConfig, type ImageTarget } from "./config";
8
+ import { encodeIco, encodePng, fromPremultiplied, type Pixels } from "./png";
9
+ import { parseSvg, type SvgSource } from "./svg";
10
+
11
+ let ready: Promise<void> | undefined;
12
+
13
+ async function rasterize(svg: string, width: number, height: number, opaque: boolean): Promise<Pixels> {
14
+ await (ready ??= initWasm(readFileSync(wasm)));
15
+ // No system fonts: sources may not contain text, so nothing depends on the machine.
16
+ const renderer = new Resvg(svg, { fitTo: { mode: "original" }, font: { loadSystemFonts: false } });
17
+ const image = renderer.render();
18
+ try {
19
+ if (image.width !== width || image.height !== height) throw new Error(`Rendered ${image.width}x${image.height} instead of ${width}x${height}.`);
20
+ return fromPremultiplied(width, height, image.pixels, opaque);
21
+ } finally { image.free(); renderer.free(); }
22
+ }
23
+
24
+ class Sources {
25
+ private readonly loaded = new Map<string, { text: string; svg?: SvgSource }>();
26
+ constructor(private readonly root: string) {}
27
+ private entry(name: string) {
28
+ let entry = this.loaded.get(name);
29
+ if (!entry) {
30
+ const file = path.join(this.root, SOURCE_DIRECTORY, `${name}.svg`);
31
+ if (!existsSync(file)) throw new Error(`Image source '${name}' does not exist at ${SOURCE_DIRECTORY}/${name}.svg.`);
32
+ entry = { text: readFileSync(file, "utf8") };
33
+ this.loaded.set(name, entry);
34
+ }
35
+ return entry;
36
+ }
37
+ text(name: string): string { return this.entry(name).text; }
38
+ parsed(names: Iterable<string>): Map<string, SvgSource> {
39
+ return new Map([...names].map(name => { const entry = this.entry(name); return [name, entry.svg ??= parseSvg(name, entry.text)]; }));
40
+ }
41
+ }
42
+
43
+ function layerSources(output: Pick<ImageOutput, "layers">): string[] {
44
+ return output.layers.flatMap(layer => "source" in layer ? [layer.source] : []);
45
+ }
46
+
47
+ async function render(output: ImageOutput, canvas: Canvas, sources: Sources): Promise<Uint8Array | string> {
48
+ const kind = format(output.path);
49
+ if (kind === "xml") {
50
+ let colors = "";
51
+ for (const [name, color] of Object.entries(output.colors!)) {
52
+ let hex = typeof color === "string" ? color : "";
53
+ if (typeof color !== "string") {
54
+ // The source's mean color over white, on a canvas of its own proportions, for settings that take a color.
55
+ const parsed = sources.parsed([color.average]), [, , viewWidth, viewHeight] = parsed.get(color.average)!.viewBox;
56
+ const width = Math.max(1, Math.round(64 * Math.min(1, viewWidth / viewHeight))), height = Math.max(1, Math.round(64 * Math.min(1, viewHeight / viewWidth)));
57
+ const average = { layers: [{ source: color.average, fit: "cover" as const, scale: null, size: null, hide: [], tint: null, bleed: [], pixelSnap: false }], mask: null, opaque: true };
58
+ const { data } = await rasterize(composeSvg(average, { width, height, factor: 1 }, parsed), width, height, true);
59
+ const sums = [0, 0, 0];
60
+ for (let index = 0; index < data.length; index += 1) sums[index % 3]! += data[index]!;
61
+ hex = `#${sums.map(sum => Math.round(sum / (width * height)).toString(16).padStart(2, "0")).join("")}`;
62
+ }
63
+ if (hex.length === 4) hex = `#${[...hex.slice(1)].map(digit => digit + digit).join("")}`;
64
+ colors += ` <color name="${name}">${hex.toUpperCase()}</color>\n`;
65
+ }
66
+ return `<?xml version="1.0" encoding="utf-8"?>\n<!-- Generated by agent-tool images from agent-tool.json. Do not edit. -->\n<resources>\n${colors}</resources>\n`;
67
+ }
68
+ const copy = verbatim(output);
69
+ // Copies pass the same checks as composed sources: no scripts, handlers, or external references.
70
+ if (copy !== null) { sources.parsed([copy]); return sources.text(copy); }
71
+ const parsed = sources.parsed(layerSources(output));
72
+ if (kind === "svg") return composeSvg(output, canvas, parsed, true);
73
+ const pixels = async (width: number, height: number) => rasterize(composeSvg(output, { width, height, factor: canvas.factor }, parsed), width, height, output.opaque);
74
+ if (kind === "ico") return encodeIco(await Promise.all(output.sizes!.map(size => pixels(size, size))));
75
+ return encodePng(await pixels(canvas.width, canvas.height));
76
+ }
77
+
78
+ function select(config: ImagesConfig, names: string[], writable: boolean): [string, ImageTarget][] {
79
+ const available = Object.keys(config.targets);
80
+ if (!available.length) throw new Error("agent-tool.json defines no images.targets.");
81
+ for (const name of names) if (!config.targets[name]) throw new Error(`Unknown image target '${name}'; expected one of: ${available.join(", ")}.`);
82
+ // Without names, build leaves targets that need --out for an explicit request.
83
+ const selected = names.length ? [...new Set(names)] : available.filter(name => !writable || config.targets[name]!.dir !== null);
84
+ if (!selected.length) throw new Error("No image target has a dir; name one and pass --out <directory>.");
85
+ return selected.map(name => [name, config.targets[name]!]);
86
+ }
87
+
88
+ /** Refuses symbolic links below the base, so a committed link cannot redirect a write outside it. */
89
+ function assertNoLinks(base: string, destination: string): void {
90
+ let current = base;
91
+ for (const part of path.relative(base, destination).split(path.sep)) {
92
+ current = path.join(current, part);
93
+ let link: boolean;
94
+ try { link = lstatSync(current).isSymbolicLink(); } catch { return; }
95
+ if (link) throw new Error(`Refusing to write through symbolic link ${path.relative(base, current)}.`);
96
+ }
97
+ }
98
+
99
+ function directory(name: string, target: ImageTarget, out: string | undefined): string {
100
+ const dir = out ?? target.dir;
101
+ if (dir === null) throw new Error(`Image target '${name}' has no dir; pass --out <directory>.`);
102
+ return dir;
103
+ }
104
+
105
+ /** Every file each selected target writes, without rendering anything. */
106
+ export function listImages(config: ImagesConfig, names: string[]) {
107
+ return {
108
+ sources: SOURCE_DIRECTORY,
109
+ targets: select(config, names, false).map(([name, target]) => ({
110
+ name, dir: target.dir, files: target.outputs.flatMap(output => expand(output).map(file => file.path)),
111
+ })),
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Renders the selected targets under the repository root. Files whose bytes
117
+ * already match are left alone, so native builds see no spurious changes, and
118
+ * the rest are replaced atomically.
119
+ */
120
+ export async function buildImages(root: string, config: ImagesConfig, names: string[], out?: string) {
121
+ if (out !== undefined && names.length !== 1) throw new Error("--out needs exactly one image target.");
122
+ const sources = new Sources(root);
123
+ // Render everything first, so a bad source or setting leaves no partial set behind.
124
+ const rendered: { name: string; dir: string; files: { relative: string; destination: string; bytes: Uint8Array }[] }[] = [];
125
+ const owners = new Map<string, string>();
126
+ for (const [name, target] of select(config, names, true)) {
127
+ const dir = directory(name, target, out);
128
+ const files = [];
129
+ for (const output of target.outputs) {
130
+ for (const file of expand(output)) {
131
+ const content = await render(output, { width: file.width, height: file.height, factor: file.factor }, sources);
132
+ const destination = path.resolve(root, dir, file.path);
133
+ // An explicit --out is the caller's choice; configured directories are checked from the repository root.
134
+ assertNoLinks(out === undefined ? root : path.resolve(root, out), destination);
135
+ // Compared without case, as macOS and Windows file systems do.
136
+ const owner = owners.get(destination.toLowerCase());
137
+ if (owner !== undefined) throw new Error(`Image targets '${owner}' and '${name}' both write ${path.relative(root, destination)}.`);
138
+ owners.set(destination.toLowerCase(), name);
139
+ files.push({ relative: path.join(dir, file.path), destination, bytes: typeof content === "string" ? Buffer.from(content) : content });
140
+ }
141
+ }
142
+ rendered.push({ name, dir, files });
143
+ }
144
+ const targets = rendered.map(({ name, dir, files }) => {
145
+ const written: string[] = [], unchanged: string[] = [];
146
+ for (const { relative, destination, bytes } of files) {
147
+ if (existsSync(destination) && Buffer.compare(readFileSync(destination), bytes) === 0) { unchanged.push(relative); continue; }
148
+ mkdirSync(path.dirname(destination), { recursive: true });
149
+ // An unpredictable name created exclusively, so no committed link can stand in for it.
150
+ const temporary = `${destination}.${randomUUID()}.tmp`;
151
+ try {
152
+ writeFileSync(temporary, bytes, { flag: "wx" });
153
+ renameSync(temporary, destination);
154
+ } catch (error) { rmSync(temporary, { force: true }); throw error; }
155
+ written.push(relative);
156
+ }
157
+ return { name, dir, written, unchanged };
158
+ });
159
+ return { targets };
160
+ }
@@ -0,0 +1,78 @@
1
+ import { pair, type ImageOutput, type SourceLayer } from "./config";
2
+ import { attribute, inlineSvg, renameReferences, type SvgSource } from "./svg";
3
+
4
+ export interface Canvas { width: number; height: number; factor: number | null }
5
+
6
+ /** Coordinates rounded so documents stay readable and byte-stable. */
7
+ function n(value: number): string { return String(Number(value.toFixed(6))); }
8
+
9
+ function nested(source: SvgSource, prefix: string, hide: Set<string>, box: [number, number, number, number], viewBox: number[], aspect: string): string {
10
+ const content = inlineSvg(source, prefix, hide);
11
+ // The source root's presentation attributes go on a group, so none can collide with or loosen the viewport's.
12
+ const presentation = source.attributes.map(([key, value]) => attribute(key, renameReferences(value, prefix))).join("");
13
+ return `<svg x="${n(box[0])}" y="${n(box[1])}" width="${n(box[2])}" height="${n(box[3])}" viewBox="${viewBox.map(n).join(" ")}" preserveAspectRatio="${aspect}" overflow="hidden">${presentation ? `<g${presentation}>${content}</g>` : content}</svg>`;
14
+ }
15
+
16
+ function placeSource(layer: SourceLayer, index: number, canvas: Canvas, source: SvgSource): string {
17
+ const { width, height } = canvas;
18
+ for (const id of layer.hide) if (!source.ids.has(id)) throw new Error(`Image source '${source.name}' has no element with id '${id}' to hide.`);
19
+ const hide = new Set(layer.hide);
20
+ const [vx, vy, vw, vh] = source.viewBox;
21
+ const [boxWidth, boxHeight] = layer.size === null ? [width * (layer.scale ?? 1), height * (layer.scale ?? 1)] : pair(layer.size).map(side => side * canvas.factor!);
22
+ const boxX = (width - boxWidth!) / 2, boxY = (height - boxHeight!) / 2;
23
+ const tint = (parts: string[]) => layer.tint === null ? parts.join("") : `<g filter="url(#agent-tool-tint-${index})">${parts.join("")}</g>`;
24
+ if (layer.fit === "cover") return tint([nested(source, `l${index}-`, hide, [boxX, boxY, boxWidth!, boxHeight!], source.viewBox, "xMidYMid slice")]);
25
+ let scale = Math.min(boxWidth! / vw, boxHeight! / vh);
26
+ if (layer.pixelSnap) {
27
+ if (!Number.isInteger(vw) || !Number.isInteger(vh)) throw new Error(`Image source '${source.name}' needs a whole-unit viewBox to snap to pixels.`);
28
+ scale = Math.floor(scale + 1e-9);
29
+ if (scale < 1) throw new Error(`Image source '${source.name}' (${vw}x${vh}) cannot snap to pixels within ${n(boxWidth!)}x${n(boxHeight!)}.`);
30
+ }
31
+ const drawnWidth = vw * scale, drawnHeight = vh * scale;
32
+ let x = (width - drawnWidth) / 2, y = (height - drawnHeight) / 2;
33
+ if (layer.pixelSnap) { x = Math.floor(x); y = Math.floor(y); }
34
+ const parts = [nested(source, `l${index}-`, hide, [x, y, drawnWidth, drawnHeight], source.viewBox, "none")];
35
+ // Bleeding repeats the source's outermost rendered pixel row or column out to the canvas edge.
36
+ const row = vh / drawnHeight, column = vw / drawnWidth;
37
+ const strips: Record<string, [[number, number, number, number], number[]]> = {
38
+ top: [[x, 0, drawnWidth, y], [vx, vy, vw, row]],
39
+ bottom: [[x, y + drawnHeight, drawnWidth, height - y - drawnHeight], [vx, vy + vh - row, vw, row]],
40
+ left: [[0, y, x, drawnHeight], [vx, vy, column, vh]],
41
+ right: [[x + drawnWidth, y, width - x - drawnWidth, drawnHeight], [vx + vw - column, vy, column, vh]],
42
+ };
43
+ for (const edge of layer.bleed) {
44
+ const [box, viewBox] = strips[edge]!;
45
+ if (box[2] > 0 && box[3] > 0) parts.push(nested(source, `l${index}${edge}-`, hide, box, viewBox, "none"));
46
+ }
47
+ return tint(parts);
48
+ }
49
+
50
+ /**
51
+ * One SVG document for an output at its pixel size. Each layer inlines its
52
+ * source in a nested viewport, so raster and SVG outputs share one geometry.
53
+ */
54
+ export function composeSvg(output: Pick<ImageOutput, "layers" | "mask" | "opaque">, canvas: Canvas, sources: ReadonlyMap<string, SvgSource>, standalone = false): string {
55
+ const { width, height } = canvas;
56
+ const namespaces: Record<string, string> = { xlink: "http://www.w3.org/1999/xlink" };
57
+ const defs: string[] = [], layers: string[] = [];
58
+ for (const [index, layer] of output.layers.entries()) {
59
+ if ("fill" in layer) { layers.push(`<rect width="${width}" height="${height}" fill="${layer.fill}"/>`); continue; }
60
+ const source = sources.get(layer.source)!;
61
+ for (const [prefix, uri] of Object.entries(source.namespaces)) {
62
+ if (namespaces[prefix] !== undefined && namespaces[prefix] !== uri) throw new Error(`Image sources bind xmlns:${prefix} to different namespaces.`);
63
+ namespaces[prefix] = uri;
64
+ }
65
+ if (layer.tint !== null) {
66
+ // User-space bounds: resvg measures nested viewports incorrectly for bounding-box filter regions.
67
+ defs.push(`<filter id="agent-tool-tint-${index}" filterUnits="userSpaceOnUse" x="0" y="0" width="${width}" height="${height}" color-interpolation-filters="sRGB"><feFlood flood-color="${layer.tint}"/><feComposite in2="SourceAlpha" operator="in"/></filter>`);
68
+ }
69
+ layers.push(placeSource(layer, index, canvas, source));
70
+ }
71
+ const shortest = Math.min(width, height);
72
+ if (output.mask === "circle") defs.push(`<clipPath id="agent-tool-mask"><circle cx="${n(width / 2)}" cy="${n(height / 2)}" r="${n(shortest / 2)}"/></clipPath>`);
73
+ else if (output.mask) defs.push(`<clipPath id="agent-tool-mask"><rect width="${width}" height="${height}" rx="${n(shortest * output.mask.cornerRadius)}"/></clipPath>`);
74
+ const size = standalone ? "" : ` width="${width}" height="${height}"`;
75
+ const declarations = Object.entries(namespaces).map(([prefix, uri]) => attribute(`xmlns:${prefix}`, uri)).join("");
76
+ const body = output.mask ? `<g clip-path="url(#agent-tool-mask)">${layers.join("")}</g>` : layers.join("");
77
+ return `<svg xmlns="http://www.w3.org/2000/svg"${declarations}${size} viewBox="0 0 ${width} ${height}">${defs.length ? `<defs>${defs.join("")}</defs>` : ""}${output.opaque ? `<rect width="${width}" height="${height}" fill="#ffffff"/>` : ""}${body}</svg>\n`;
78
+ }
@@ -0,0 +1,205 @@
1
+ /** The data-only `images` section of agent-tool.json: named targets of rendered brand images. */
2
+
3
+ export type Size = number | [number, number];
4
+ export type Edge = "top" | "right" | "bottom" | "left";
5
+ export interface FillLayer { fill: string }
6
+ export interface SourceLayer {
7
+ source: string;
8
+ fit: "contain" | "cover";
9
+ scale: number | null;
10
+ size: Size | null;
11
+ hide: string[];
12
+ tint: string | null;
13
+ bleed: Edge[];
14
+ pixelSnap: boolean;
15
+ }
16
+ export type ImageLayer = FillLayer | SourceLayer;
17
+ export type ColorValue = string | { average: string };
18
+ export interface ImageOutput {
19
+ path: string;
20
+ size: Size | null;
21
+ variants: Record<string, number> | Record<string, [number, number]> | null;
22
+ sizes: number[] | null;
23
+ opaque: boolean;
24
+ mask: "circle" | { cornerRadius: number } | null;
25
+ layers: ImageLayer[];
26
+ colors: Record<string, ColorValue> | null;
27
+ }
28
+ export interface ImageTarget { dir: string | null; outputs: ImageOutput[] }
29
+ export interface ImagesConfig { targets: Record<string, ImageTarget> }
30
+
31
+ /** Sources live at assets/brand/<name>.svg in every repository. */
32
+ export const SOURCE_DIRECTORY = "assets/brand";
33
+ export const ANDROID_DENSITIES = { mdpi: 1, hdpi: 1.5, xhdpi: 2, xxhdpi: 3, xxxhdpi: 4 };
34
+ export const MAX_PIXELS = 8192;
35
+ const NAME = /^[a-z][a-z0-9-]*$/;
36
+ const COLOR = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
37
+ const ID = /^[A-Za-z_][\w.:-]*$/;
38
+ const EDGES: Edge[] = ["top", "right", "bottom", "left"];
39
+ const FORMATS = ["png", "ico", "svg", "xml"] as const;
40
+ export type ImageFormat = typeof FORMATS[number];
41
+
42
+ type Json = Record<string, unknown>;
43
+ function fail(setting: string): never { throw new Error(`Invalid config setting '${setting}'.`); }
44
+ function object(value: unknown, setting: string, allowed: string[]): Json {
45
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(setting);
46
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new Error(`Unknown ${setting} setting '${key}'.`);
47
+ return value as Json;
48
+ }
49
+ /** An object whose keys are names chosen by the project. */
50
+ function record(value: unknown, setting: string): Json {
51
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(setting);
52
+ return value as Json;
53
+ }
54
+ function positive(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value > 0; }
55
+ function size(value: unknown, setting: string): Size {
56
+ if (positive(value)) return value;
57
+ if (Array.isArray(value) && value.length === 2 && value.every(positive)) return [value[0], value[1]];
58
+ fail(setting);
59
+ }
60
+ export function pair(value: Size): [number, number] { return typeof value === "number" ? [value, value] : value; }
61
+ /** Repository-relative and inside it: no absolute paths, backslashes, or dot segments. */
62
+ export function relativePath(value: unknown): value is string {
63
+ return typeof value === "string" && !value.startsWith("/") && !value.includes("\\") && value.split("/").every(part => part && part !== "." && part !== "..");
64
+ }
65
+ export function format(path: string): ImageFormat {
66
+ return path.slice(path.lastIndexOf(".") + 1) as ImageFormat;
67
+ }
68
+
69
+ function parseLayer(value: unknown, setting: string): ImageLayer {
70
+ if (value && typeof value === "object" && "fill" in value) {
71
+ const layer = object(value, setting, ["fill"]);
72
+ if (typeof layer.fill !== "string" || !COLOR.test(layer.fill)) fail(`${setting}.fill`);
73
+ return { fill: layer.fill };
74
+ }
75
+ const layer = object(value, setting, ["source", "fit", "scale", "size", "hide", "tint", "bleed", "pixelSnap"]);
76
+ if (typeof layer.source !== "string" || !NAME.test(layer.source)) fail(`${setting}.source`);
77
+ const fit = layer.fit ?? "contain";
78
+ if (fit !== "contain" && fit !== "cover") fail(`${setting}.fit`);
79
+ if (layer.scale !== undefined && (!positive(layer.scale) || layer.scale > 4)) fail(`${setting}.scale`);
80
+ if (layer.scale !== undefined && layer.size !== undefined) throw new Error(`${setting} sets both scale and size; choose one.`);
81
+ const hide = layer.hide ?? [];
82
+ if (!Array.isArray(hide) || hide.some(id => typeof id !== "string" || !ID.test(id)) || new Set(hide).size !== hide.length) fail(`${setting}.hide`);
83
+ if (layer.tint !== undefined && (typeof layer.tint !== "string" || !COLOR.test(layer.tint))) fail(`${setting}.tint`);
84
+ const bleed = layer.bleed ?? [];
85
+ if (!Array.isArray(bleed) || bleed.some(edge => !EDGES.includes(edge)) || new Set(bleed).size !== bleed.length) fail(`${setting}.bleed`);
86
+ if (layer.pixelSnap !== undefined && typeof layer.pixelSnap !== "boolean") fail(`${setting}.pixelSnap`);
87
+ const result: SourceLayer = {
88
+ source: layer.source, fit, scale: layer.scale ?? null, size: layer.size === undefined ? null : size(layer.size, `${setting}.size`),
89
+ hide, tint: layer.tint ?? null, bleed, pixelSnap: layer.pixelSnap ?? false,
90
+ };
91
+ if (fit === "cover" && (result.bleed.length || result.pixelSnap)) throw new Error(`${setting} can only bleed or snap to pixels with fit "contain".`);
92
+ return result;
93
+ }
94
+
95
+ function parseOutput(value: unknown, setting: string): ImageOutput {
96
+ const input = object(value, setting, ["path", "size", "variants", "sizes", "opaque", "mask", "layers", "colors"]);
97
+ if (!relativePath(input.path) || !FORMATS.includes(format(input.path))) fail(`${setting}.path`);
98
+ const path = input.path, kind = format(path);
99
+ const allowed: Record<ImageFormat, string[]> = {
100
+ png: ["size", "variants", "opaque", "mask", "layers"],
101
+ svg: ["size", "variants", "opaque", "mask", "layers"],
102
+ ico: ["sizes", "opaque", "mask", "layers"],
103
+ xml: ["colors"],
104
+ };
105
+ for (const key of Object.keys(input)) if (key !== "path" && !allowed[kind].includes(key)) throw new Error(`${setting} is ${kind} output and cannot set '${key}'.`);
106
+
107
+ let variants: ImageOutput["variants"] = null;
108
+ if (input.variants !== undefined) {
109
+ const entries = Object.entries(input.variants === "android" ? ANDROID_DENSITIES : record(input.variants, `${setting}.variants`));
110
+ const factors = entries.every(([, factor]) => positive(factor)), pairs = entries.every(([, dimensions]) => Array.isArray(dimensions));
111
+ if (!entries.length || (!factors && !pairs) || entries.some(([key]) => !/^[\w@.-]*$/.test(key))) fail(`${setting}.variants`);
112
+ variants = Object.fromEntries(entries.map(([key, entry]) => [key, factors ? entry : pair(size(entry, `${setting}.variants.${key}`))])) as ImageOutput["variants"];
113
+ if (!path.includes("{variant}")) throw new Error(`${setting}.path needs {variant} to name each variant's file.`);
114
+ } else if (path.includes("{variant}")) throw new Error(`${setting}.path uses {variant} without variants.`);
115
+ const explicit = variants !== null && Object.values(variants).every(Array.isArray);
116
+ const base = input.size === undefined ? null : size(input.size, `${setting}.size`);
117
+ if (explicit && base !== null) throw new Error(`${setting} gives each variant its pixels; remove size.`);
118
+
119
+ let sizes: number[] | null = null;
120
+ if (kind === "ico") {
121
+ if (!Array.isArray(input.sizes) || !input.sizes.length || input.sizes.some(item => !Number.isInteger(item) || item < 1 || item > 256) || new Set(input.sizes).size !== input.sizes.length) fail(`${setting}.sizes`);
122
+ sizes = input.sizes as number[];
123
+ }
124
+ let colors: ImageOutput["colors"] = null;
125
+ if (kind === "xml") {
126
+ const entries = Object.entries(record(input.colors, `${setting}.colors`));
127
+ if (!entries.length) fail(`${setting}.colors`);
128
+ colors = {};
129
+ for (const [name, color] of entries) {
130
+ if (!/^[a-z_][a-z0-9_]*$/.test(name)) fail(`${setting}.colors.${name}`);
131
+ if (typeof color === "string" && COLOR.test(color)) colors[name] = color;
132
+ else {
133
+ const average = object(color, `${setting}.colors.${name}`, ["average"]).average;
134
+ if (typeof average !== "string" || !NAME.test(average)) fail(`${setting}.colors.${name}`);
135
+ colors[name] = { average };
136
+ }
137
+ }
138
+ return { path, size: null, variants: null, sizes: null, opaque: false, mask: null, layers: [], colors };
139
+ }
140
+
141
+ if (input.opaque !== undefined && typeof input.opaque !== "boolean") fail(`${setting}.opaque`);
142
+ let mask: ImageOutput["mask"] = null;
143
+ if (input.mask === "circle") mask = "circle";
144
+ else if (input.mask !== undefined) {
145
+ const radius = object(input.mask, `${setting}.mask`, ["cornerRadius"]).cornerRadius;
146
+ if (!positive(radius) || radius > 0.5) fail(`${setting}.mask.cornerRadius`);
147
+ mask = { cornerRadius: radius };
148
+ }
149
+ if (!Array.isArray(input.layers) || !input.layers.length) fail(`${setting}.layers`);
150
+ const layers = input.layers.map((layer, index) => parseLayer(layer, `${setting}.layers[${index}]`));
151
+ if ((explicit || kind === "ico") && layers.some(layer => "source" in layer && layer.size !== null)) throw new Error(`${setting} has no base size for a layer size; use scale.`);
152
+ const output: ImageOutput = { path, size: base, variants, sizes, opaque: input.opaque ?? false, mask, layers, colors };
153
+ if (kind === "png" && base === null && !explicit) throw new Error(`${setting} needs a size or sized variants.`);
154
+ if (kind === "svg" && base === null && !explicit && !verbatim(output)) throw new Error(`${setting} needs a size unless it copies one source unchanged.`);
155
+ for (const { width, height } of base === null && !explicit ? [] : expand(output)) {
156
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width > MAX_PIXELS || height > MAX_PIXELS) throw new Error(`${setting} renders ${width}x${height}; sizes must be whole pixels up to ${MAX_PIXELS}.`);
157
+ }
158
+ return output;
159
+ }
160
+
161
+ /** An SVG output that is one unmodified source is copied byte for byte. */
162
+ export function verbatim(output: ImageOutput): string | null {
163
+ const [layer] = output.layers;
164
+ if (format(output.path) !== "svg" || output.layers.length !== 1 || output.size !== null || output.variants !== null || output.opaque || output.mask || !layer || !("source" in layer)) return null;
165
+ return layer.fit === "contain" && layer.scale === null && layer.size === null && !layer.hide.length && layer.tint === null && !layer.bleed.length && !layer.pixelSnap ? layer.source : null;
166
+ }
167
+
168
+ export function parseImages(value: unknown): ImagesConfig {
169
+ const input = object(value, "images", ["targets"]);
170
+ const targets = record(input.targets ?? {}, "images.targets");
171
+ const result: ImagesConfig = { targets: {} };
172
+ for (const [name, entry] of Object.entries(targets)) {
173
+ const setting = `images.targets.${name}`;
174
+ if (!NAME.test(name)) fail(setting);
175
+ const target = object(entry, setting, ["dir", "outputs"]);
176
+ if (target.dir !== undefined && !relativePath(target.dir)) fail(`${setting}.dir`);
177
+ if (!Array.isArray(target.outputs) || !target.outputs.length) fail(`${setting}.outputs`);
178
+ const outputs = target.outputs.map((output, index) => parseOutput(output, `${setting}.outputs[${index}]`));
179
+ const paths = outputs.flatMap(output => expand(output).map(file => file.path));
180
+ const escaping = paths.find(path => !relativePath(path));
181
+ if (escaping !== undefined) throw new Error(`${setting} writes '${escaping}', which is not a path inside its directory.`);
182
+ // Compared without case, as macOS and Windows file systems do.
183
+ const folded = paths.map(path => path.toLowerCase());
184
+ const repeated = paths.find((_, index) => folded.indexOf(folded[index]!) !== index);
185
+ if (repeated) throw new Error(`${setting} writes ${repeated} more than once.`);
186
+ result.targets[name] = { dir: target.dir ?? null, outputs };
187
+ }
188
+ return result;
189
+ }
190
+
191
+ export interface ImageFile { path: string; width: number; height: number; factor: number | null }
192
+
193
+ /** One file per variant, with its pixel size and the factor that scales layer sizes. */
194
+ export function expand(output: ImageOutput): ImageFile[] {
195
+ if (output.variants === null) {
196
+ const [width, height] = output.size === null ? [0, 0] : pair(output.size);
197
+ return [{ path: output.path, width, height, factor: 1 }];
198
+ }
199
+ return Object.entries(output.variants).map(([key, value]) => {
200
+ const path = output.path.replaceAll("{variant}", key);
201
+ if (Array.isArray(value)) return { path, width: value[0], height: value[1], factor: null };
202
+ const [width, height] = pair(output.size!).map(side => side * value) as [number, number];
203
+ return { path, width, height, factor: value };
204
+ });
205
+ }
@@ -0,0 +1,110 @@
1
+ import { deflateSync } from "node:zlib";
2
+
3
+ export interface Pixels { width: number; height: number; channels: 3 | 4; data: Uint8Array }
4
+
5
+ const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, index) => {
6
+ let value = index;
7
+ for (let bit = 0; bit < 8; bit += 1) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
8
+ return value >>> 0;
9
+ });
10
+ function crc32(bytes: Uint8Array): number {
11
+ let crc = 0xffffffff;
12
+ for (const byte of bytes) crc = CRC_TABLE[(crc ^ byte) & 0xff]! ^ (crc >>> 8);
13
+ return (crc ^ 0xffffffff) >>> 0;
14
+ }
15
+
16
+ function chunk(type: string, data: Uint8Array): Uint8Array {
17
+ const bytes = new Uint8Array(data.length + 12);
18
+ const view = new DataView(bytes.buffer);
19
+ view.setUint32(0, data.length);
20
+ bytes.set(new TextEncoder().encode(type), 4);
21
+ bytes.set(data, 8);
22
+ view.setUint32(data.length + 8, crc32(bytes.subarray(4, data.length + 8)));
23
+ return bytes;
24
+ }
25
+
26
+ function paeth(left: number, up: number, upLeft: number): number {
27
+ const estimate = left + up - upLeft;
28
+ const toLeft = Math.abs(estimate - left), toUp = Math.abs(estimate - up), toUpLeft = Math.abs(estimate - upLeft);
29
+ return toLeft <= toUp && toLeft <= toUpLeft ? left : toUp <= toUpLeft ? up : upLeft;
30
+ }
31
+
32
+ /**
33
+ * Premultiplied RGBA from the renderer to straight-alpha RGBA, or to RGB when
34
+ * the image is opaque: app stores reject icons that carry an alpha channel.
35
+ */
36
+ export function fromPremultiplied(width: number, height: number, rgba: Uint8Array, opaque: boolean): Pixels {
37
+ const channels = opaque ? 3 : 4;
38
+ const data = new Uint8Array(width * height * channels);
39
+ for (let pixel = 0; pixel < width * height; pixel += 1) {
40
+ const alpha = rgba[pixel * 4 + 3]!;
41
+ if (opaque && alpha !== 255) throw new Error("An opaque image rendered a transparent pixel.");
42
+ for (let channel = 0; channel < 3; channel += 1) {
43
+ const value = rgba[pixel * 4 + channel]!;
44
+ data[pixel * channels + channel] = alpha === 0 ? 0 : alpha === 255 ? value : Math.min(255, Math.round(value * 255 / alpha));
45
+ }
46
+ if (!opaque) data[pixel * 4 + 3] = alpha;
47
+ }
48
+ return { width, height, channels, data };
49
+ }
50
+
51
+ /** A deterministic PNG: no timestamps or text, and each row's filter chosen by the usual minimum-sum heuristic. */
52
+ export function encodePng({ width, height, channels, data }: Pixels): Uint8Array {
53
+ const stride = width * channels;
54
+ const filtered = new Uint8Array((stride + 1) * height);
55
+ const candidates = Array.from({ length: 5 }, () => new Uint8Array(stride));
56
+ const empty = new Uint8Array(stride);
57
+ for (let row = 0; row < height; row += 1) {
58
+ const current = data.subarray(row * stride, (row + 1) * stride);
59
+ const previous = row ? data.subarray((row - 1) * stride, row * stride) : empty;
60
+ let best = 0, bestScore = Infinity;
61
+ for (let type = 0; type < 5; type += 1) {
62
+ const output = candidates[type]!;
63
+ let score = 0;
64
+ for (let index = 0; index < stride; index += 1) {
65
+ const left = index >= channels ? current[index - channels]! : 0;
66
+ const up = previous[index]!, upLeft = index >= channels ? previous[index - channels]! : 0;
67
+ const predictor = type === 0 ? 0 : type === 1 ? left : type === 2 ? up : type === 3 ? (left + up) >> 1 : paeth(left, up, upLeft);
68
+ const value = (current[index]! - predictor) & 0xff;
69
+ output[index] = value;
70
+ score += value < 128 ? value : 256 - value;
71
+ if (score >= bestScore) break;
72
+ }
73
+ if (score < bestScore) { best = type; bestScore = score; }
74
+ }
75
+ filtered[row * (stride + 1)] = best;
76
+ filtered.set(candidates[best]!, row * (stride + 1) + 1);
77
+ }
78
+ const header = new DataView(new ArrayBuffer(13));
79
+ header.setUint32(0, width);
80
+ header.setUint32(4, height);
81
+ header.setUint8(8, 8);
82
+ header.setUint8(9, channels === 3 ? 2 : 6);
83
+ return Buffer.concat([
84
+ Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10),
85
+ chunk("IHDR", new Uint8Array(header.buffer)),
86
+ chunk("IDAT", deflateSync(filtered, { level: 9 })),
87
+ chunk("IEND", new Uint8Array()),
88
+ ]);
89
+ }
90
+
91
+ /** A Windows icon that stores one PNG per size, which every current browser reads. */
92
+ export function encodeIco(images: readonly Pixels[]): Uint8Array {
93
+ const pngs = images.map(encodePng);
94
+ const directory = new DataView(new ArrayBuffer(6 + images.length * 16));
95
+ directory.setUint16(2, 1, true);
96
+ directory.setUint16(4, images.length, true);
97
+ let offset = directory.byteLength;
98
+ for (const [index, image] of images.entries()) {
99
+ const entry = 6 + index * 16;
100
+ // A stored dimension of 0 means 256 pixels.
101
+ directory.setUint8(entry, image.width % 256);
102
+ directory.setUint8(entry + 1, image.height % 256);
103
+ directory.setUint16(entry + 4, 1, true);
104
+ directory.setUint16(entry + 6, 32, true);
105
+ directory.setUint32(entry + 8, pngs[index]!.length, true);
106
+ directory.setUint32(entry + 12, offset, true);
107
+ offset += pngs[index]!.length;
108
+ }
109
+ return Buffer.concat([new Uint8Array(directory.buffer), ...pngs]);
110
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Just enough XML to inline brand sources in a composed document: one copy per
3
+ * layer with prefixed ids, named elements dropped, and features refused when
4
+ * they would render differently between machines, leak between layers, or
5
+ * script or fetch when an output is opened.
6
+ */
7
+
8
+ export interface SvgSource {
9
+ name: string;
10
+ viewBox: [number, number, number, number];
11
+ /** Root presentation attributes, such as fill or shape-rendering, that the inlined copy keeps. */
12
+ attributes: [string, string][];
13
+ namespaces: Record<string, string>;
14
+ ids: Set<string>;
15
+ tokens: Token[];
16
+ }
17
+
18
+ type Token =
19
+ | { kind: "raw"; text: string }
20
+ | { kind: "start"; name: string; attributes: [string, string][]; selfClosing: boolean }
21
+ | { kind: "end"; name: string };
22
+
23
+ const TOKEN = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<\?[\s\S]*?\?>|<!DOCTYPE[^>[]*(\[[\s\S]*?\])?\s*>|<\/([A-Za-z_][\w.:-]*)\s*>|<([A-Za-z_][\w.:-]*)((?:\s+[^\s=/>]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>|[^<]+/y;
24
+ const ATTRIBUTE = /\s+([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
25
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
26
+ // Static SVG only: shapes, paint servers, clipping, masking, and filters. Animation, links, and markup
27
+ // from other namespaces could script or fetch when an output is opened, so they are refused, not trusted.
28
+ const ELEMENTS = new Set([
29
+ "circle", "clipPath", "defs", "desc", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite",
30
+ "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA",
31
+ "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset",
32
+ "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "g", "image", "line",
33
+ "linearGradient", "marker", "mask", "metadata", "path", "pattern", "polygon", "polyline", "radialGradient", "rect",
34
+ "stop", "svg", "switch", "symbol", "title", "use",
35
+ ]);
36
+ const REASONS: Record<string, string> = {
37
+ style: "a <style> element; use presentation attributes",
38
+ text: "text, which depends on installed fonts; convert it to paths",
39
+ };
40
+ const ROOT_GEOMETRY = new Set(["xmlns", "version", "baseProfile", "width", "height", "x", "y", "viewBox", "preserveAspectRatio", "overflow", "id"]);
41
+
42
+ const ENTITIES: Record<string, string> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
43
+
44
+ /** Attribute values as an XML parser sees them, so checks cannot be bypassed by encoding. */
45
+ function decode(name: string, value: string): string {
46
+ if (/&(?!(?:#x[0-9a-fA-F]{1,6}|#[0-9]{1,7}|amp|lt|gt|quot|apos);)/.test(value)) throw new Error(`Image source '${name}' uses an undefined entity in '${value.slice(0, 40)}'.`);
47
+ const decoded = value.replace(/&(?:#x([0-9a-fA-F]+)|#([0-9]+)|(amp|lt|gt|quot|apos));/g, (reference, hex?: string, decimal?: string, entity?: string) => {
48
+ if (entity) return ENTITIES[entity]!;
49
+ const code = hex ? Number.parseInt(hex, 16) : Number(decimal);
50
+ if (code < 1 || code > 0x10ffff) throw new Error(`Image source '${name}' has an invalid character reference '${reference}'.`);
51
+ return String.fromCodePoint(code);
52
+ });
53
+ // CSS escapes could spell url( in a way the reference check cannot see; SVG values never need them.
54
+ if (decoded.includes("\\")) throw new Error(`Image source '${name}' uses a CSS escape in '${value.slice(0, 40)}'.`);
55
+ return decoded;
56
+ }
57
+
58
+ function tokenize(name: string, text: string): Token[] {
59
+ const tokens: Token[] = [];
60
+ TOKEN.lastIndex = 0;
61
+ while (TOKEN.lastIndex < text.length) {
62
+ const match = TOKEN.exec(text);
63
+ if (!match) throw new Error(`Image source '${name}' is not well-formed SVG near offset ${TOKEN.lastIndex}.`);
64
+ if (match[1] !== undefined) throw new Error(`Image source '${name}' declares a DOCTYPE subset; remove it.`);
65
+ if (match[0].startsWith("<!DOCTYPE") && /["']/.test(match[0]) && !/^<!DOCTYPE\s+svg\s+PUBLIC\s+"[^"]*"\s+"http:\/\/www\.w3\.org\/[^"]*"\s*>$/.test(match[0])) {
66
+ throw new Error(`Image source '${name}' names a DOCTYPE other than the W3C SVG DTD; remove it.`);
67
+ }
68
+ if (match[2] !== undefined) tokens.push({ kind: "end", name: match[2] });
69
+ else if (match[3] !== undefined) {
70
+ const attributes: [string, string][] = [];
71
+ for (const attribute of match[4]!.matchAll(ATTRIBUTE)) {
72
+ if (attributes.some(([key]) => key === attribute[1])) throw new Error(`Image source '${name}' repeats attribute '${attribute[1]}'.`);
73
+ attributes.push([attribute[1]!, decode(name, attribute[2] ?? attribute[3]!)]);
74
+ }
75
+ tokens.push({ kind: "start", name: match[3], attributes, selfClosing: match[5] === "/" });
76
+ } else tokens.push({ kind: "raw", text: match[0] });
77
+ }
78
+ return tokens;
79
+ }
80
+
81
+ /** The name without its namespace prefix: any prefix may be bound to the SVG or XLink namespace. */
82
+ function local(name: string): string { return name.slice(name.indexOf(":") + 1); }
83
+
84
+ /** Event handlers, stylesheets, and references to anything outside the file except embedded images, are refused. */
85
+ function checkAttributes(name: string, element: string, attributes: [string, string][]): void {
86
+ for (const [key, value] of attributes) {
87
+ if (/^on/i.test(local(key))) throw new Error(`Image source '${name}' has a '${key}' event handler.`);
88
+ if (local(key) === "style") throw new Error(`Image source '${name}' has a style attribute; use presentation attributes.`);
89
+ if (local(key) === "base") throw new Error(`Image source '${name}' sets '${key}', which could resolve references elsewhere.`);
90
+ // CSS image functions can fetch without url(), for example through a mask presentation attribute.
91
+ if (key !== "id" && /(?:image-set|image|cross-fade|element|src)\s*\(/i.test(value)) throw new Error(`Image source '${name}' uses a CSS image function in '${value.slice(0, 40)}'.`);
92
+ const external = local(key) === "href"
93
+ ? !value.startsWith("#") && !(["image", "feImage"].includes(local(element)) && value.startsWith("data:"))
94
+ : value.split(/url\(/i).slice(1).some(target => !target.trimStart().replace(/^['"]/, "").startsWith("#"));
95
+ if (external) throw new Error(`Image source '${name}' references '${value.slice(0, 40)}'; embed it or reference an element in the file.`);
96
+ }
97
+ }
98
+
99
+ interface Scope { default: string | undefined; prefixes: Record<string, string> }
100
+ function declare(parent: Scope, attributes: [string, string][]): Scope {
101
+ const declarations = attributes.filter(([key]) => key === "xmlns" || key.startsWith("xmlns:"));
102
+ if (!declarations.length) return parent;
103
+ const scope: Scope = { default: parent.default, prefixes: { ...parent.prefixes } };
104
+ for (const [key, value] of declarations) if (key === "xmlns") scope.default = value; else scope.prefixes[key.slice(6)] = value;
105
+ return scope;
106
+ }
107
+
108
+ function number(value: string | undefined): number | undefined {
109
+ const parsed = value === undefined ? NaN : Number(value.trim().replace(/px$/, ""));
110
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
111
+ }
112
+
113
+ export function parseSvg(name: string, text: string): SvgSource {
114
+ const all = tokenize(name, text);
115
+ const start = all.findIndex(token => token.kind !== "raw");
116
+ const root = all[start];
117
+ const prologue = all.slice(0, Math.max(start, 0));
118
+ if (!root || root.kind !== "start" || root.name !== "svg" || prologue.some(token => token.kind === "raw" && !/^(?:\s+|<!--[\s\S]*-->|<\?xml\s[\s\S]*\?>|<!DOCTYPE[\s\S]*>)$/.test(token.text))) {
119
+ throw new Error(`Image source '${name}' must have an <svg> root element.`);
120
+ }
121
+ const rootAttributes = new Map(root.attributes);
122
+ checkAttributes(name, root.name, root.attributes);
123
+ if (rootAttributes.get("xmlns") !== SVG_NAMESPACE) throw new Error(`Image source '${name}' must declare xmlns="${SVG_NAMESPACE}".`);
124
+ const box = rootAttributes.get("viewBox")?.trim().split(/[\s,]+/).map(Number);
125
+ const width = number(rootAttributes.get("width")), height = number(rootAttributes.get("height"));
126
+ let viewBox: [number, number, number, number];
127
+ if (box) {
128
+ if (box.length !== 4 || box.some(value => !Number.isFinite(value)) || box[2]! <= 0 || box[3]! <= 0) throw new Error(`Image source '${name}' has an invalid viewBox.`);
129
+ viewBox = box as [number, number, number, number];
130
+ } else if (width && height) viewBox = [0, 0, width, height];
131
+ else throw new Error(`Image source '${name}' needs a viewBox or numeric width and height.`);
132
+
133
+ const tokens: Token[] = [], ids = new Set<string>(), stack: string[] = [], scopes = [declare({ default: undefined, prefixes: {} }, root.attributes)];
134
+ // Ends on the root's closing tag, or on the root itself when it is self-closing.
135
+ let index = start;
136
+ if (!root.selfClosing) {
137
+ for (index = start + 1; index < all.length; index += 1) {
138
+ const token = all[index]!;
139
+ if (token.kind === "end") {
140
+ if (stack.length === 0) { if (token.name !== "svg") throw new Error(`Image source '${name}' closes </${token.name}> without opening it.`); break; }
141
+ if (stack.pop() !== token.name) throw new Error(`Image source '${name}' closes </${token.name}> out of order.`);
142
+ scopes.pop();
143
+ } else if (token.kind === "start") {
144
+ const scope = declare(scopes.at(-1)!, token.attributes);
145
+ const prefix = token.name.includes(":") ? token.name.slice(0, token.name.indexOf(":")) : undefined;
146
+ if ((prefix === undefined ? scope.default : scope.prefixes[prefix]) !== SVG_NAMESPACE || !ELEMENTS.has(local(token.name))) {
147
+ throw new Error(`Image source '${name}' contains ${REASONS[local(token.name)] ?? `an unsupported <${token.name}> element; keep only static SVG shapes, paint, clipping, masking, and filters`}.`);
148
+ }
149
+ checkAttributes(name, token.name, token.attributes);
150
+ for (const [key, value] of token.attributes) if (key === "id") { if (ids.has(value)) throw new Error(`Image source '${name}' repeats id '${value}'.`); ids.add(value); }
151
+ if (!token.selfClosing) { stack.push(token.name); scopes.push(scope); }
152
+ } else if (token.text.startsWith("<?")) throw new Error(`Image source '${name}' contains a processing instruction.`);
153
+ tokens.push(token);
154
+ }
155
+ if (index === all.length) throw new Error(`Image source '${name}' never closes its <svg> root.`);
156
+ }
157
+ if (all.slice(index + 1).some(token => token.kind !== "raw" || !/^(?:\s+|<!--[\s\S]*-->)$/.test(token.text))) throw new Error(`Image source '${name}' has content after its <svg> root.`);
158
+
159
+ const namespaces: Record<string, string> = {};
160
+ const attributes: [string, string][] = [];
161
+ for (const [key, value] of root.attributes) {
162
+ if (key.startsWith("xmlns:")) namespaces[key.slice(6)] = value;
163
+ else if (!ROOT_GEOMETRY.has(key)) attributes.push([key, value]);
164
+ }
165
+ return { name, viewBox, attributes, namespaces, ids, tokens };
166
+ }
167
+
168
+ export function attribute(key: string, value: string): string {
169
+ return ` ${key}="${value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll('"', "&quot;")}"`;
170
+ }
171
+
172
+ /** Points `url(#id)` references at a copy whose ids carry the prefix. */
173
+ export function renameReferences(value: string, prefix: string): string {
174
+ return value.replace(/url\(\s*(['"]?)#([^'")\s]+)\1\s*\)/gi, (_, _quote, id: string) => `url(#${prefix}${id})`);
175
+ }
176
+
177
+ /** The source's children with every id prefixed, internal references rewritten, and hidden elements removed. */
178
+ export function inlineSvg(source: SvgSource, prefix: string, hide: ReadonlySet<string>): string {
179
+ const rename = (value: string) => renameReferences(value, prefix);
180
+ let markup = "", skipped = 0;
181
+ for (const token of source.tokens) {
182
+ if (token.kind === "start") {
183
+ if (skipped > 0 || token.attributes.some(([key, value]) => key === "id" && hide.has(value))) { if (!token.selfClosing) skipped += 1; continue; }
184
+ markup += `<${token.name}`;
185
+ for (const [key, value] of token.attributes) {
186
+ if (key === "id") markup += attribute(key, `${prefix}${value}`);
187
+ else if (local(key) === "href" && value.startsWith("#")) markup += attribute(key, `#${prefix}${value.slice(1)}`);
188
+ else markup += attribute(key, rename(value));
189
+ }
190
+ markup += token.selfClosing ? "/>" : ">";
191
+ } else if (token.kind === "end") {
192
+ if (skipped > 0) skipped -= 1;
193
+ else markup += `</${token.name}>`;
194
+ } else if (skipped === 0) markup += token.text;
195
+ }
196
+ return markup;
197
+ }
@@ -0,0 +1,5 @@
1
+ // Bun embeds the file in compiled executables and resolves it from node_modules otherwise.
2
+ declare module "@resvg/resvg-wasm/index_bg.wasm" {
3
+ const path: string;
4
+ export default path;
5
+ }
@@ -5,5 +5,5 @@ export const BUNDLED_SKILLS: Readonly<Record<string, string>> = {
5
5
  "reset": "---\nname: reset\ndescription: Return a clean checkout to its updated default branch after confirmed shipping or an explicit checkout cleanup request.\n---\n\n# Reset\n\nThis is checkout cleanup, not a destructive Git reset. Read repository guidance\nfor setup and hooks. Determine the default branch through the repository's\nremote metadata rather than assuming main.\n\nCheck worktree and index first. Preserve user edits; if they prevent switching,\nreport the condition instead of discarding or automatically stashing them. For\npost-merge cleanup, confirm the PR is MERGED before removing a feature branch.\n\nFetch the default branch, switch to it, and update by fast-forward only. A\ndiverged default branch requires inspection rather than forced replacement.\nDelete a merged feature branch only within the requested cleanup scope and\nafter verifying its exact commit is the shipped commit; squash merging does not\nmake Git ancestry-based branch deletion proof of shipping. Honor keep-branch.\n\nRun only the hook installer or dependency setup documented by this repository\nwhen the updated checkout requires it. Do not assume a package manager or copy\nsetup commands from another project. Report final branch, commit, and any\nunfinished cleanup.\n",
6
6
  "ship-pr": "---\nname: ship-pr\ndescription: Complete an authorized commit, independent review, repair, PR, CI, and squash-merge workflow for a GitHub repository.\n---\n\n# Ship PR\n\nUse the installed `agent-tool`. Read repository guidance and policy first;\nvalidation, versioning, package management, and hook installation remain project\nconcerns. Shipping authorizes this workflow within the user's stated scope.\nPreserve unrelated edits and honor requested stopping points such as open-PR\nonly, report-only review, or keeping the feature branch.\n\nUse `agent-tool run --label <check> -- <command> [args...]` for noisy setup,\nvalidation, builds, and hook-running Git commands. It preserves exit status and\ncomplete private logs, prints one success receipt, and shows a bounded failure\ntail. Inspect more with `agent-tool logs <log> --max-bytes 8192 --tail-lines 80`\nor a targeted search; never dump an entire suite log or pipe checks into `tail`.\nThe receipt proves process completion, not absence of warnings: inspect required\nwarning, coverage, timing, or artifact evidence before accepting a check. Keep\nmachine-readable receipts and review findings visible by running `versions\nprepare`, `review`, and other commands whose output drives a decision directly.\nPrefer documented compact test reporters when they preserve required evidence;\ndo not reduce test scope, skip checks, silence warnings, or mask failures.\n\n1. Finish the requested change on a feature branch. Create it with\n `git switch --no-track -c <branch>`; settings such as\n `branch.autosetupmerge=always` otherwise make it track a local branch.\n Validate it with appropriate project checks and commit only the intended\n files.\n2. Determine the current PR's base repository and branch, or the repository's\n default branch when no PR exists. Fetch and pin its exact base. Integrate it\n using the repository's normal workflow, resolving conflicts without losing\n user work. If project policy versions packages, prepare versions against the\n pinned base after every integration or repair and before each review, using\n the project's commands or `agent-tool versions prepare <base-oid>`, which\n commits bumped manifests and the refreshed lockfile.\n `agent-tool versions resolve-conflicts` resolves conflicts confined to\n version fields. Review the resulting committed HEAD with an independent\n reviewer using the bundled cross-agent-review skill. Repair blocking findings\n and review every changed HEAD again. Review failure blocks shipping.\n3. Push the reviewed HEAD through normal hooks, then verify the pushed SHA still\n equals the reviewed SHA. If a hook changed content or HEAD, review that result.\n Open the PR using the open-pr skill, or update the existing PR as needed.\n4. Allow any configured review bots time to respond, then fetch unresolved PR\n review threads. Follow repository policy for feedback: fix valid findings,\n reply in the original threads, and resolve only fully addressed comments.\n Validate, commit, push, and independently review every feedback or CI repair.\n Wait for CI, recheck the live base, PR base branch, and local and remote heads.\n A changed base requires integration, validation, and another review. When\n versions are managed, `agent-tool versions check <base-oid>` must pass for the\n live base. Use the squash-merge skill to merge the exact reviewed HEAD.\n5. After confirmed MERGED, honor keep-branch; otherwise use the reset skill to\n return to the updated default branch and apply any documented project setup.\n6. When the project deploys the merged branch, verify the deployment for the\n merge commit and run its documented live checks. Report deployment failures\n separately from a successful merge; follow project policy for repairs.\n\nNever infer a clean review from process success alone or carry a review across\ncontent changes. Do not weaken configured CI policy to get a merge through.\nIf a step fails, preserve useful intermediate work and report the concrete\nremaining issue. Report PR link, merged commit, review verdict, validation, and\nfinal checkout state.\n",
7
7
  "squash-merge": "---\nname: squash-merge\ndescription: Squash-merge a reviewed GitHub pull request while binding the mutation to the reviewed commit and checking CI.\n---\n\n# Squash Merge\n\nUse the installed `agent-tool` and repository merge policy. Merge only within\nthe user's authorized scope. Have the exact reviewed HEAD, base commit, and base\nbranch available, and read the verdict and unresolved findings. BLOCKER or MAJOR\nfindings require repair and a new review before merging.\nFollow the repository's review-bot policy and require all actionable blocking\nreview feedback to be addressed before merging.\n\nWait for required checks using `gh pr checks --watch --fail-fast`. Refresh the\nPR and live base from the repository that owns the PR. Confirm local HEAD and PR\nHEAD both equal the reviewed commit, the base branch is unchanged, the live base\nstill equals the reviewed base, and that base is an ancestor of reviewed HEAD.\nIf any identity changed, refresh, validate, and review again.\n\nUse a title that satisfies `agent-tool.json` and pass both reviewed HEAD and\nbase branch:\n\n```sh\nagent-tool pr merge 'feat: describe the change' <reviewed-head-oid> <base-branch>\n```\n\nThe helper enforces HEAD atomically through GitHub's `expectedHeadOid`, checks\nCI, rejects queued or automatic merges, and creates a subject-only squash with\nthe PR reference. GitHub has no atomic expected-base argument in this mutation;\nrepository branch protection must enforce any required base freshness. With\n`merge.requireStrictBaseFreshness`, the helper refuses to merge unless an\nactive strict status ruleset that the actor cannot bypass protects the base. Do\nnot retarget the PR concurrently with merging.\n\nConfirm GitHub reports MERGED before any cleanup. A failed or uncertain mutation\nis not evidence of a merge. Preserve the branch on failure. After a confirmed\nmerge, perform checkout cleanup only when requested or as part of an authorized\nshipping workflow. Report the PR and merge result.\n",
8
- "update-dependencies": "---\nname: update-dependencies\ndescription: Update a repository's dependency manifests, lockfiles, runtimes, tools, and CI pins with upstream migrations, compatibility checks, and non-destructive infrastructure previews.\n---\n\n# Update Dependencies\n\nUse the repository's package managers and installed `agent-tool`. Read `AGENTS.md`,\nREADME, linked setup/release documents, and repository-owned upgrade skills before\nediting. Follow their hooks, version policy, checks, and requested stopping point.\nUpdate applicable dependencies throughout the repository, including workspaces\nand examples. This skill does not authorize deploying, merging, publishing, or\nchanging global tools; use existing user authorization and `ship-pr` when shipping\nis requested. Default dependency-update merge subject: `chore: update dependencies`,\nsubject to repository title policy and any explicit user subject.\n\nRun noisy installs, checks, tests, and builds with\n`agent-tool run --label <check> -- <command> [args...]` to retain full private\nlogs and return the original status with compact output. Inspect bounded excerpts\nusing `agent-tool logs <log>` and targeted searches for warnings and evidence;\na successful receipt does not establish a warning-free upgrade. Keep structured\naudit/plan results, deployment previews, and review findings directly visible\nwhen they drive a safety decision. Do not dump full suite logs into context,\nsilence warnings at their source, or change check scope to reduce output.\n\n## Safety before execution\n\n**Always dry-run before any infrastructure apply or deployment. Never destroy,\nreplace, or recreate a resource during an upgrade.** This includes Terraform,\nOpenTofu, Wrangler, Ansible, other infrastructure tools, and effects triggered by\ninstallation, tests, hooks, Git push, opening a PR, CI, or merging. Greenfield\nprojects and approval to ship do not waive this rule.\n\nBefore running project commands or pushing, trace their scripts, hooks, workflow\ntriggers, reusable workflows, and deployment commands. A dry-run flag alone is\ninsufficient: inspect tasks, provisioners, plugins, external programs, and actions\nthat can mutate despite preview mode. Avoid such a preview unless its effects\nare proven read-only. Do not run destroy/delete, taint/replace, state removal,\nbackend migration, forced database reset, or an automatic \"repair\" that bypasses\nthe rule. Do not hide destruction with targeting, disabled refresh, ignored\nchanges, new resource identities, or weakened policy.\n\nPreview with the proposed versions and complete configuration for each affected\nenvironment, using its real backend/workspace/account, variables, inventory, and\nresource identities. An empty local state, mocked credentials, validate-only run,\nor bundle build cannot establish safety for production. Check the current baseline\nas well when needed to distinguish existing drift from the upgrade. Keep state,\nbinary plans, JSON plans, and secret-bearing diffs out of Git and public reports.\n\nIf a preview proposes deletion or replacement, fails, is incomplete, omits\naffected resources, or cannot establish safety, skip that dependency and its\ncoupled migrations. Restore only this task's edits to the affected compatibility\ngroup, retain independent safe upgrades, and record the reason and affected\nresources. Never apply to find out. A preview is not deployment authorization.\n\nBefore an authorized apply/deploy, verify the exact validated commit, toolchain,\nconfiguration, environment, and current state still match the preview. Re-preview\nafter relevant changes or drift. CI must enforce the same order and stop on an\nunsafe preview before mutation; a successful local preview does not excuse an\nunguarded push- or merge-triggered deployment. Establish a narrowly scoped guard\nwhen authorized and reviewable, or hold the affected upgrade before triggering\nthat workflow. Do not disable protections to ship. A plan must be inspected before\nthe operation; inspecting deployment logs afterward is too late.\nAn unsafe workflow triggered on every branch push also blocks pushing otherwise\nindependent upgrades until that trigger has a proven safe guard.\n\n### Terraform and OpenTofu\n\nRead core, provider, and module upgrade guides, including intermediate major\nmigrations. Regenerate provider locks with the pinned CLI, preserving supported\nplatform hashes. Backend initialization/migration is also subject to the safety\ngate. Inspect external data programs and provisioners before planning.\n\nUse a full refreshed saved plan with locking and the real variables, for example\n`terraform plan -input=false -out=<private-plan>`; if using `-detailed-exitcode`,\n0 and 2 are success, 1 is failure. Inspect `terraform show -json <private-plan>`.\nRun `agent-tool dependencies check-terraform-plan <private-plan.json>` to check\nsupported JSON resource actions. It fails for every action containing `delete`,\nincluding both replacement orders, unknown actions, incomplete/deferred plans,\nfailed checks, or opaque provider action invocations. It prints actions and\ndiagnostics without resource attribute values; it does not prove that a plan is\nfresh, comprehensive, authentic, or that provider code has no hidden effects.\nOlder/unsupported plan formats that cannot establish completeness are skipped.\nOpenTofu currently omits `complete` in its JSON format and is rejected by this\nhelper; report that limitation rather than adding a synthetic completeness flag.\nAlso inspect resource identity, drift, warnings, provider semantics, and any\neffects outside resource actions. For an authorized apply, consume the exact\napproved saved plan, not an uninspected newly generated plan.\n\nSource: [Terraform JSON plan format](https://developer.hashicorp.com/terraform/internals/json-format).\n\n### Wrangler and other deployment tools\n\nUse the proposed pinned Wrangler for `wrangler deploy --dry-run` with the same\nbuild/configuration/environment used by deployment. This checks bundling, not a\ncomplete remote resource plan. Separately verify account, worker name, routes,\nbindings and resource IDs against the existing deployment and read migration\nsemantics. Review Durable Object deleted/renamed classes, D1 schema/data\nmigrations, storage resources, queues, containers, and deployment integrations.\nNever infer \"no destruction\" from a successful bundle. Resource removals,\nreplacement, or unprovable remote effects skip the affected group. Apply the\nequivalent preview and identity checks to other tools rather than assuming they\nshare Terraform's guarantees.\n\nDo not assume `wrangler d1 migrations list --remote` is read-only. The\nimplementation inspected for this skill initializes migration tracking with\n`CREATE TABLE IF NOT EXISTS`; verify the pinned Wrangler version before using\nthe command. Verify database identity and compare migration files with the\ndatabase's configured migration tracking table through audited read-only\nqueries. A missing table, pending migration, or unverifiable schema/data\neffects hold the affected upgrade/deployment group. Never initialize schema\nmerely to establish preview safety.\n\nWrangler runs a custom `build.command` again during deployment. Use supported\nconfiguration referencing the same validated artifacts for both preview and\ndeployment, preventing another unvalidated build before upload. Bind the\nartifacts, configuration, and pinned tool to the authorized commit, and recheck\ntheir identities before upload and after relevant changes. Hold the affected\ngroup if the pinned Wrangler cannot upload the validated output without\nrebuilding it, or if the upload cannot be proven to match the preview.\n\nSources: [Wrangler command documentation](https://developers.cloudflare.com/workers/wrangler/commands/),\n[D1 migration listing implementation](https://github.com/cloudflare/workers-sdk/blob/e3d186776fcc2e4b5cc4cfd7155591778a375957/packages/wrangler/src/d1/migrations/list.ts),\n[migration table initialization](https://github.com/cloudflare/workers-sdk/blob/e3d186776fcc2e4b5cc4cfd7155591778a375957/packages/wrangler/src/d1/migrations/helpers.ts),\nand [Wrangler custom builds](https://developers.cloudflare.com/workers/wrangler/custom-builds/).\n\n### Ansible\n\nCheck controller Python, ansible-core, collections/roles, and target Python support\ntogether; read porting guides. Inspect all tasks and invoked scripts before\n`ansible-playbook --check --diff` with the real inventory and variables.\n`check_mode: false` forces mutation even with `--check`; unsupported modules and\nconditions based on registered results can omit effects. Inspect deletion,\nrecreation, service/data reset, and package removal semantics beyond the diff.\nSkipped or opaque tasks are a safety gap, not a clean preview. Redact diffs.\n\nSource: [Ansible check mode limits](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_checkmode.html).\n\n## Inventory and choose compatible targets\n\nRecord each dependency source, current resolved version, latest stable candidate,\nselected target, compatibility constraints, upstream migration links, and checks.\nQuery authoritative registries, official releases, support matrices, and migration\nguides at execution time; do not treat remembered versions or prereleases as\nlatest stable. Inspect runtime requirements, peer dependencies, ABI/API changes,\nplatform availability, maintained release lines, and changed defaults, including\nsecurity, privacy/telemetry, data retention, and postinstall/native behavior.\nCheck transitive dependencies' peer/runtime constraints as well as direct ones. Unsupported\nor unpublished targets stay pinned with an explanation.\n\nInventory manifests, locks, tool configuration, CI/reusable actions, containers,\nscripts, deployment files, and executable documentation. Search duplicate version\npins and aliases, not just filenames; exclude vendored/generated snapshots unless\nthe repository owns their update process. Examples of sources to inspect:\n\n| Ecosystem | Sources and compatibility group |\n| --- | --- |\n| JavaScript/TypeScript | All package.json dependency fields, overrides/resolutions, workspace catalogs, packageManager/engines, npm/Bun/pnpm/Yarn locks, Node/Bun pins, compiler and lint/build/test plugins |\n| Toolchains | mise.toml/.mise.toml, mise.lock, .tool-versions, runtime version files, tool URLs/checksums, CI pins and container images; include every tool declared by mise, not just mise itself |\n| Python/Ansible | pyproject/requirements, uv/Poetry/pip locks, Python pins, ansible-core, collection/role requirements, lint plugins and managed host constraints |\n| Ruby/iOS | .ruby-version, Gemfile/lock, Bundler, CocoaPods, Podfile/lock, podspecs, Swift packages and Xcode/iOS deployment requirements |\n| Infrastructure | Terraform/OpenTofu CLI constraints, provider locks/constraints, module versions/Git refs, Wrangler and Cloudflare integration dependencies |\n| CI/system/native | Actions/reusable workflow refs, Docker tags/digests, OS images/packages, Rust/Cargo toolchain and locks, Go modules, Gradle/AGP/Kotlin/JDK and other repository-specific manifests |\n\nBuild connected compatibility groups before editing. Select the newest supported\ncombination, which can be older than an individual package's latest. Upgrade the\nframework first or keep its compatible toolchain pinned; do not force peer\nresolution, drop compatibility checks, or add blanket overrides to make installs\npass. Preserve source pin policy (exact version, digest, full Git SHA) and resolve\nimmutable pins from verified upstream releases. Preserve intentional runtime\nminimums unless migration requires changing the support contract.\n\nFor React Native, use its version-specific upgrade guide/template diffs and\nrepository upgrade skills. Couple React and its renderer, native modules,\nMetro/Babel, CLI, codegen, Hermes/New Architecture, CocoaPods/Bundler/Ruby,\nXcode/iOS deployment targets, and Android Gradle/AGP/Kotlin/JDK/SDK/NDK. For Expo,\nuse the chosen SDK's supported React Native/dependency matrix and its dependency\nchecks. Do not independently update framework-managed pods or generated native\nfiles; use the framework's regeneration workflow and prove iOS/Android builds.\nPermissive peer ranges alone do not prove native renderer or toolchain compatibility;\ncheck the framework's actual supported versions and native implementation.\nAn unavailable native toolchain is a reported validation limit; do not claim the\ngroup verified. See [React Native upgrading](https://reactnative.dev/docs/upgrading).\n\nCheck developer tools' own dependencies too: a compiler release without its old\nprogrammatic API may break test/lint runners that import it even when `tsc` passes.\nAction runtime upgrades can require newer self-hosted runners; preserve workflow\npermissions, action inputs/outputs, caching, artifact semantics, and job names.\n\nCheck current advisories with read-only ecosystem audits against the baseline and\nproposed lockfiles, using the repository's configured registry/advisory sources.\nDo not send private dependency metadata to an unconfigured or unauthorized service;\nreport the resulting coverage limit. Cover direct and resolved transitive dependencies,\nincluding native/platform-specific binaries. Latest direct packages can still\npin vulnerable transitive versions. Trace each finding to its owning dependency,\nverify affected versions, runtime conditions, and remediation in the maintainer's\nadvisory and fixed release. When the owner's range permits a fixed version, use a\ntargeted refresh with the lockfile's owning manager and preserve unrelated\nresolutions; otherwise prefer a supported owner upgrade.\nIf the owner still pins a vulnerable version, a narrowly scoped security patch\noverride may be used only with API/ABI/runtime compatibility evidence and focused\nregression checks of the actual affected integration. Document its rationale and\nremoval condition; a passing install or bundle alone is insufficient. Do not run\nblind audit fixes or use blanket overrides to bypass compatibility constraints.\nReport unresolved advisories and unavailable audit coverage accurately; current\ndirect pins or a partial clean audit do not\nestablish that the whole dependency graph is safe.\n\n## Migrate and validate\n\nCapture relevant baseline checks and warnings first, after screening their side\neffects. Update one compatibility group at a time. Read the upstream instructions\nfor every breaking step; implement required source/config/schema migrations and\nremoved API replacements. Use the manager that owns each lockfile, on the selected\nruntime, and inspect the resulting diff. Refresh transitive dependencies through\ntheir owning direct dependency; do not hand-edit integrity hashes or lock entries.\nReview intentional patches, overrides, and pins before removal; retain them until\nupstream fixes and regression checks demonstrate they are unnecessary.\nAvoid unrelated global installations or environment changes. Use task-local or\nrepository-scoped tools for testing changed runtime pins.\nFor published packages, also validate tarballs and consumer installs with their\ndocumented supported package managers, including manifest/override syntax and\nruntime requirements. A successful owning-manager install does not prove that\npublished metadata works for those consumers.\n\nRun the repository's setup/hooks, frozen installs, lint, typechecks, tests, builds,\npackage/platform smoke checks, and relevant safe integration previews. Resolve\nupgrade-related deprecation warnings at their source. Do not suppress warnings,\nweaken tests, or disable checks; report remaining upstream/pre-existing warnings\nwith evidence and keep a target pinned when migration cannot be completed.\nAdd focused regression coverage when a migration changes behavior; avoid tests\nthat merely assert version strings. Re-run checks after repairs. Report checks\nblocked by missing credentials, platforms, or external services accurately.\nRe-audit the final resolved graph after migrations and repairs.\n\nConfirm every inventory row is upgraded, already current, intentionally constrained,\nor skipped with a concrete reason. Do not present an unresolved group as complete.\nWhen upgrading agent-tool itself, install its managed skills with the same harness\nscope and commit the ownership manifest; never edit generated managed skill copies.\n\nWhen changes depend on another repository's release, ship the producer first and\nconfirm publishing and registry availability before pinning the consumer; include\nits exact version/commit and API migration notes. Independent groups may proceed.\nUse the installed `ship-pr` for authorized shipping and independently review every\nchanged HEAD. Check deployment safety again before its push/open/merge operations.\n\n## Report\n\nKeep a compact ledger of old/selected versions, completed migrations, validation\nand warning results, preview environment/commit/tool versions and safe action\nsummary, and every skipped/constrained dependency with the reason. Include baseline\nand final audit results, security overrides with their rationale and removal\ncondition, unresolved advisories, and audit coverage limits. For shipping,\ninclude branch, reviewed commit/verdict, PR, merge, publish/deploy result, and final\ncheckout state. For a repository sweep, keep one entry per repository, including\nunready or unchanged checkouts. Do not publish credentials, state, or plan contents.\n"
8
+ "update-dependencies": "---\nname: update-dependencies\ndescription: Update a repository's dependency manifests, lockfiles, runtimes, tools, and CI pins with upstream migrations, compatibility checks, and non-destructive infrastructure previews.\n---\n\n# Update Dependencies\n\nUse the repository's package managers and installed `agent-tool`. Read `AGENTS.md`,\nREADME, existing dependency-maintenance notes, linked setup/release documents,\nand repository-owned upgrade skills before editing. Follow their hooks, version\npolicy, checks, and requested stopping point.\nUpdate applicable dependencies throughout the repository, including workspaces\nand examples. This skill does not authorize deploying, merging, publishing, or\nchanging global tools; use existing user authorization and `ship-pr` when shipping\nis requested. Default dependency-update merge subject: `chore: update dependencies`,\nsubject to repository title policy and any explicit user subject.\n\nRun noisy installs, checks, tests, and builds with\n`agent-tool run --label <check> -- <command> [args...]` to retain full private\nlogs and return the original status with compact output. Inspect bounded excerpts\nusing `agent-tool logs <log>` and targeted searches for warnings and evidence;\na successful receipt does not establish a warning-free upgrade. Keep structured\naudit/plan results, deployment previews, and review findings directly visible\nwhen they drive a safety decision. Do not dump full suite logs into context,\nsilence warnings at their source, or change check scope to reduce output.\n\n## Safety before execution\n\n**Always dry-run before any infrastructure apply or deployment. Never destroy,\nreplace, or recreate a resource during an upgrade.** This includes Terraform,\nOpenTofu, Wrangler, Ansible, other infrastructure tools, and effects triggered by\ninstallation, tests, hooks, Git push, opening a PR, CI, or merging. Greenfield\nprojects and approval to ship do not waive this rule.\n\nBefore running project commands or pushing, trace their scripts, hooks, workflow\ntriggers, reusable workflows, and deployment commands. A dry-run flag alone is\ninsufficient: inspect tasks, provisioners, plugins, external programs, and actions\nthat can mutate despite preview mode. Avoid such a preview unless its effects\nare proven read-only. Do not run destroy/delete, taint/replace, state removal,\nbackend migration, forced database reset, or an automatic \"repair\" that bypasses\nthe rule. Do not hide destruction with targeting, disabled refresh, ignored\nchanges, new resource identities, or weakened policy.\n\nPreview with the proposed versions and complete configuration for each affected\nenvironment, using its real backend/workspace/account, variables, inventory, and\nresource identities. An empty local state, mocked credentials, validate-only run,\nor bundle build cannot establish safety for production. Check the current baseline\nas well when needed to distinguish existing drift from the upgrade. Keep state,\nbinary plans, JSON plans, and secret-bearing diffs out of Git and public reports.\n\nIf a preview proposes deletion or replacement, fails, is incomplete, omits\naffected resources, or cannot establish safety, skip that dependency and its\ncoupled migrations. Restore only this task's edits to the affected compatibility\ngroup, retain independent safe upgrades, and record the reason and affected\nresources. Never apply to find out. A preview is not deployment authorization.\n\nBefore an authorized apply/deploy, verify the exact validated commit, toolchain,\nconfiguration, environment, and current state still match the preview. Re-preview\nafter relevant changes or drift. CI must enforce the same order and stop on an\nunsafe preview before mutation; a successful local preview does not excuse an\nunguarded push- or merge-triggered deployment. Establish a narrowly scoped guard\nwhen authorized and reviewable, or hold the affected upgrade before triggering\nthat workflow. Do not disable protections to ship. A plan must be inspected before\nthe operation; inspecting deployment logs afterward is too late.\nAn unsafe workflow triggered on every branch push also blocks pushing otherwise\nindependent upgrades until that trigger has a proven safe guard.\n\n### Terraform and OpenTofu\n\nRead core, provider, and module upgrade guides, including intermediate major\nmigrations. Regenerate provider locks with the pinned CLI, preserving supported\nplatform hashes. Backend initialization/migration is also subject to the safety\ngate. Inspect external data programs and provisioners before planning.\n\nUse a full refreshed saved plan with locking and the real variables, for example\n`terraform plan -input=false -out=<private-plan>`; if using `-detailed-exitcode`,\n0 and 2 are success, 1 is failure. Inspect `terraform show -json <private-plan>`.\nRun `agent-tool dependencies check-terraform-plan <private-plan.json>` to check\nsupported JSON resource actions. It fails for every action containing `delete`,\nincluding both replacement orders, unknown actions, incomplete/deferred plans,\nfailed checks, or opaque provider action invocations. It prints actions and\ndiagnostics without resource attribute values; it does not prove that a plan is\nfresh, comprehensive, authentic, or that provider code has no hidden effects.\nOlder/unsupported plan formats that cannot establish completeness are skipped.\nOpenTofu currently omits `complete` in its JSON format and is rejected by this\nhelper; report that limitation rather than adding a synthetic completeness flag.\nAlso inspect resource identity, drift, warnings, provider semantics, and any\neffects outside resource actions. For an authorized apply, consume the exact\napproved saved plan, not an uninspected newly generated plan.\n\nSource: [Terraform JSON plan format](https://developer.hashicorp.com/terraform/internals/json-format).\n\n### Wrangler and other deployment tools\n\nUse the proposed pinned Wrangler for `wrangler deploy --dry-run` with the same\nbuild/configuration/environment used by deployment. This checks bundling, not a\ncomplete remote resource plan. Separately verify account, worker name, routes,\nbindings and resource IDs against the existing deployment and read migration\nsemantics. Review Durable Object deleted/renamed classes, D1 schema/data\nmigrations, storage resources, queues, containers, and deployment integrations.\nNever infer \"no destruction\" from a successful bundle. Resource removals,\nreplacement, or unprovable remote effects skip the affected group. Apply the\nequivalent preview and identity checks to other tools rather than assuming they\nshare Terraform's guarantees.\n\nDo not assume `wrangler d1 migrations list --remote` is read-only. The\nimplementation inspected for this skill initializes migration tracking with\n`CREATE TABLE IF NOT EXISTS`; verify the pinned Wrangler version before using\nthe command. Verify database identity and compare migration files with the\ndatabase's configured migration tracking table through audited read-only\nqueries. A missing table, pending migration, or unverifiable schema/data\neffects hold the affected upgrade/deployment group. Never initialize schema\nmerely to establish preview safety.\n\nWrangler runs a custom `build.command` again during deployment. Use supported\nconfiguration referencing the same validated artifacts for both preview and\ndeployment, preventing another unvalidated build before upload. Bind the\nartifacts, configuration, and pinned tool to the authorized commit, and recheck\ntheir identities before upload and after relevant changes. Hold the affected\ngroup if the pinned Wrangler cannot upload the validated output without\nrebuilding it, or if the upload cannot be proven to match the preview.\n\nSources: [Wrangler command documentation](https://developers.cloudflare.com/workers/wrangler/commands/),\n[D1 migration listing implementation](https://github.com/cloudflare/workers-sdk/blob/e3d186776fcc2e4b5cc4cfd7155591778a375957/packages/wrangler/src/d1/migrations/list.ts),\n[migration table initialization](https://github.com/cloudflare/workers-sdk/blob/e3d186776fcc2e4b5cc4cfd7155591778a375957/packages/wrangler/src/d1/migrations/helpers.ts),\nand [Wrangler custom builds](https://developers.cloudflare.com/workers/wrangler/custom-builds/).\n\n### Ansible\n\nCheck controller Python, ansible-core, collections/roles, and target Python support\ntogether; read porting guides. Inspect all tasks and invoked scripts before\n`ansible-playbook --check --diff` with the real inventory and variables.\n`check_mode: false` forces mutation even with `--check`; unsupported modules and\nconditions based on registered results can omit effects. Inspect deletion,\nrecreation, service/data reset, and package removal semantics beyond the diff.\nSkipped or opaque tasks are a safety gap, not a clean preview. Redact diffs.\n\nSource: [Ansible check mode limits](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_checkmode.html).\n\n## Inventory and choose compatible targets\n\nRecord each dependency source, current resolved version, latest stable candidate,\nselected target, compatibility constraints, upstream migration links, and checks\nin the run ledger described under Report.\nQuery authoritative registries, official releases, support matrices, and migration\nguides at execution time; do not treat remembered versions or prereleases as\nlatest stable. Inspect runtime requirements, peer dependencies, ABI/API changes,\nplatform availability, maintained release lines, and changed defaults, including\nsecurity, privacy/telemetry, data retention, and postinstall/native behavior.\nCheck transitive dependencies' peer/runtime constraints as well as direct ones. Unsupported\nor unpublished targets stay pinned with an explanation.\n\nInventory manifests, locks, tool configuration, CI/reusable actions, containers,\nscripts, deployment files, and executable documentation. Search duplicate version\npins and aliases, not just filenames; exclude vendored/generated snapshots unless\nthe repository owns their update process. Examples of sources to inspect:\n\n| Ecosystem | Sources and compatibility group |\n| --- | --- |\n| JavaScript/TypeScript | All package.json dependency fields, overrides/resolutions, workspace catalogs, packageManager/engines, npm/Bun/pnpm/Yarn locks, Node/Bun pins, compiler and lint/build/test plugins |\n| Toolchains | mise.toml/.mise.toml, mise.lock, .tool-versions, runtime version files, tool URLs/checksums, CI pins and container images; include every tool declared by mise, not just mise itself |\n| Python/Ansible | pyproject/requirements, uv/Poetry/pip locks, Python pins, ansible-core, collection/role requirements, lint plugins and managed host constraints |\n| Ruby/iOS | .ruby-version, Gemfile/lock, Bundler, CocoaPods, Podfile/lock, podspecs, Swift packages and Xcode/iOS deployment requirements |\n| Infrastructure | Terraform/OpenTofu CLI constraints, provider locks/constraints, module versions/Git refs, Wrangler and Cloudflare integration dependencies |\n| CI/system/native | Actions/reusable workflow refs, Docker tags/digests, OS images/packages, Rust/Cargo toolchain and locks, Go modules, Gradle/AGP/Kotlin/JDK and other repository-specific manifests |\n\nBuild connected compatibility groups before editing. Select the newest supported\ncombination, which can be older than an individual package's latest. Upgrade the\nframework first or keep its compatible toolchain pinned; do not force peer\nresolution, drop compatibility checks, or add blanket overrides to make installs\npass. Preserve source pin policy (exact version, digest, full Git SHA) and resolve\nimmutable pins from verified upstream releases. Preserve intentional runtime\nminimums unless migration requires changing the support contract.\n\nFor React Native, use its version-specific upgrade guide/template diffs and\nrepository upgrade skills. Couple React and its renderer, native modules,\nMetro/Babel, CLI, codegen, Hermes/New Architecture, CocoaPods/Bundler/Ruby,\nXcode/iOS deployment targets, and Android Gradle/AGP/Kotlin/JDK/SDK/NDK. For Expo,\nuse the chosen SDK's supported React Native/dependency matrix and its dependency\nchecks. Do not independently update framework-managed pods or generated native\nfiles; use the framework's regeneration workflow and prove iOS/Android builds.\nPermissive peer ranges alone do not prove native renderer or toolchain compatibility;\ncheck the framework's actual supported versions and native implementation.\nAn unavailable native toolchain is a reported validation limit; do not claim the\ngroup verified. See [React Native upgrading](https://reactnative.dev/docs/upgrading).\n\nCheck developer tools' own dependencies too: a compiler release without its old\nprogrammatic API may break test/lint runners that import it even when `tsc` passes.\nAction runtime upgrades can require newer self-hosted runners; preserve workflow\npermissions, action inputs/outputs, caching, artifact semantics, and job names.\n\nCheck current advisories with read-only ecosystem audits against the baseline and\nproposed lockfiles, using the repository's configured registry/advisory sources.\nDo not send private dependency metadata to an unconfigured or unauthorized service;\nreport the resulting coverage limit. Cover direct and resolved transitive dependencies,\nincluding native/platform-specific binaries. Latest direct packages can still\npin vulnerable transitive versions. Trace each finding to its owning dependency,\nverify affected versions, runtime conditions, and remediation in the maintainer's\nadvisory and fixed release. When the owner's range permits a fixed version, use a\ntargeted refresh with the lockfile's owning manager and preserve unrelated\nresolutions; otherwise prefer a supported owner upgrade.\nIf the owner still pins a vulnerable version, a narrowly scoped security patch\noverride may be used only with API/ABI/runtime compatibility evidence and focused\nregression checks of the actual affected integration. Document its rationale and\nremoval condition; a passing install or bundle alone is insufficient. Do not run\nblind audit fixes or use blanket overrides to bypass compatibility constraints.\nReport unresolved advisories and unavailable audit coverage accurately; current\ndirect pins or a partial clean audit do not\nestablish that the whole dependency graph is safe.\n\n## Migrate and validate\n\nCapture relevant baseline checks and warnings first, after screening their side\neffects. Update one compatibility group at a time. Read the upstream instructions\nfor every breaking step; implement required source/config/schema migrations and\nremoved API replacements. Use the manager that owns each lockfile, on the selected\nruntime, and inspect the resulting diff. Refresh transitive dependencies through\ntheir owning direct dependency; do not hand-edit integrity hashes or lock entries.\nReview intentional patches, overrides, and pins before removal; retain them until\nupstream fixes and regression checks demonstrate they are unnecessary.\nAvoid unrelated global installations or environment changes. Use task-local or\nrepository-scoped tools for testing changed runtime pins.\nFor published packages, also validate tarballs and consumer installs with their\ndocumented supported package managers, including manifest/override syntax and\nruntime requirements. A successful owning-manager install does not prove that\npublished metadata works for those consumers.\n\nRun the repository's setup/hooks, frozen installs, lint, typechecks, tests, builds,\npackage/platform smoke checks, and relevant safe integration previews. Resolve\nupgrade-related deprecation warnings at their source. Do not suppress warnings,\nweaken tests, or disable checks; report remaining upstream/pre-existing warnings\nwith evidence and keep a target pinned when migration cannot be completed.\nAdd focused regression coverage when a migration changes behavior; avoid tests\nthat merely assert version strings. Re-run checks after repairs. Report checks\nblocked by missing credentials, platforms, or external services accurately.\nRe-audit the final resolved graph after migrations and repairs.\n\nConfirm every inventory row is upgraded, already current, intentionally constrained,\nor skipped with a concrete reason. Do not present an unresolved group as complete.\nWhen upgrading agent-tool itself, install its managed skills with the same harness\nscope and commit the ownership manifest; never edit generated managed skill copies.\n\nWhen changes depend on another repository's release, ship the producer first and\nconfirm publishing and registry availability before pinning the consumer; include\nits exact version/commit and API migration notes. Independent groups may proceed.\nUse the installed `ship-pr` for authorized shipping and independently review every\nchanged HEAD. Check deployment safety again before its push/open/merge operations.\n\n## Report\n\nKeep run-specific evidence in the upgrade PR body or a linked PR comment: the\ninventory and old/selected versions, completed migrations, validation and warnings,\nbaseline/final audits and coverage limits, preview environment/commit/tool versions\nand safe action summary, and skipped/constrained dependencies with reasons. Include\nsecurity overrides, their rationale and removal condition, and unresolved advisories.\nIf no PR is requested, provide this ledger in the task report; reporting does not\nauthorize opening a PR or issue. For a sweep, keep one entry per repository,\nincluding unready and unchanged checkouts, and use an existing tracking issue only\nwhen explicitly requested. Never create an issue merely to store upgrade evidence.\n\nKeep durable repository guidance focused on what the next upgrade needs: current\ncompatibility constraints, intentional pins/overrides, unresolved advisories, and\nreusable repository-specific procedures. Prefer the existing maintenance document;\notherwise create `docs/dependency-upgrades.md` (or the repository's equivalent)\nonly when there is substantive guidance to preserve. For each constraint, record\nits rationale, upstream advisory/migration/issue link or originating PR, and the\ncondition and check for removing or revisiting it. Do not copy version inventories,\naudit output, validation/preview receipts, or dated run journals into that document.\n\nOn every upgrade, reconcile the existing maintenance document against the final\ndependency graph and checks: update constraints in place, remove resolved entries,\nand link the PR for historical evidence. Keep the root README focused on purpose,\nsetup, supported requirements, and normal usage, with a concise maintenance link\nwhen needed. Keep dependency-maintenance guidance in `AGENTS.md` to a short\nrouting rule and link; do not accumulate upgrade reports in either entrypoint\nor duplicate the maintenance document.\n\nFor shipping, report the branch, reviewed commit/verdict, PR, merge, publish/deploy\nresult, and final checkout state. Keep full logs private; never publish credentials,\nstate, or plan contents in any of these locations.\n"
9
9
  };