Ships the docs for **paperclip v2026.720.0** to `docs.paperclip.ing`. Window: `v2026.707.0...v2026.720.0` — 196 commits, 1,523 changed files, 0 truncated leaves. Most of this release was already drafted on `nightly` over the past cycle (Cases, Folders, Decision Training, Attention, Built-in Agents, company skill policy, inbox agent policy, `costStatus`, the ACP engine graduation and `acpx_local` retirement, and `PAPERCLIP_TOOL_ACTION_SIGNING_SECRET`). This PR adds the surfaces nightly had **not** yet covered, then stamps the release version across everything touched this cycle. ## New pages **`docs/reference/api/tool-gateway.md`** — the MCP Tool Gateway, the headline experimental feature. Covers applications and connections, catalog entries and risk levels, profiles/entries/bindings, the tool-access policy and its decision outcomes, named MCP gateways and their tokens, the audit feed, and the Smoke Lab. Documents the five `tools:*` permission keys and the viewer/membership rules on routes that don't use a permission key. Flags both experimental gates (`enableApps`, and `enableSmokeLab` separately). **`docs/reference/api/summary-slots.md`** — the built-in Summarizer and summary slots. Covers slot addressing (`scopeKind` / `scopeId` / `slotKey`), generation, revisions, and the `enableSummaries` gate. Cross-references Built-in Agents rather than duplicating it. ## Updated **`docs/guides/org/skills.md`** — Skill Studio (the three-pane authoring workspace, saved inputs, test runs and their unavailability reasons, run templates, version history), nested skill folders and the My Skills view, importing skills straight from a project, and company skill forks. Cross-references the existing Folders and Company Skill Policy API pages instead of restating them. ## Release stamping `paperclip_version: v2026.720.0` applied to all 26 pages touched this cycle — bumped on 10 that already had frontmatter, added to 13 that had none (including the API pages nightly authored, which were intentionally versionless until they merged to `main`). ## ↻ Renames detected `detect-renames.mjs` reports no directory renames. It flags one `removed_dirs_no_match`: - `packages/adapters/acpx-local` — **already reconciled on nightly.** The adapter was retired upstream (ACP became a capability of each harness adapter rather than a standalone adapter). `docs/reference/adapters/acpx-local.md` is now a retired stub that points at Claude Code / Codex / Gemini CLI and documents the automatic migration. No action needed. ## Screenshots — full overhaul recommended **248 of 298** registry entries are stale. This release landed a design-system convergence (single-source design tokens, retuned theme, broad Card/Badge adoption, a multiplicative radius ladder, unified list surfaces, Lucide task-status glyphs), so nearly every captured surface moved. This is an overhaul cycle, not a targeted refresh: ```sh npm run screenshots:refresh:all ``` Full per-entry breakdown in `SCREENSHOTS_PENDING.md`. Captures go to their own PR for review — nothing is auto-pushed. ## Verification - **Drift:** clean — 0 records against `903bd15` (8 parent paths, 57 CLI commands, 66 env vars, 389 REST routes, 18 permission keys, 4 roles). - **Claim verification:** `verify-edit.mjs` against `v2026.720.0` — **0 unverified, 0 suspicious** across all three authored files. - **Reconciliation:** no disappeared manifest entries. - **`npm run docs:build`:** passes — 169 markdown files, sitemap and robots generated. - **`npm run sync:check`:** links clean (172 files), nav clean, screenshots indexed, drift clean. - **Nav orphans:** `docs/reference/cli/commands.md` and `docs/reference/cli/control-plane-commands.md` — intentional redirect anchors, expected. `.sync-state.json` bumped to `base_release_tag: v2026.720.0` / `base_release_sha: 903bd157…`, which also repoints the drift checker's pinned ref (the previous `f12bb27…` no longer resolves upstream). No open `nightly-draft/*` PRs to fold in. ## Checklist - [ ] Tool Gateway page reads correctly for an operator enabling `enableApps` for the first time - [ ] Summary Slots experimental gating is accurate - [ ] Skill Studio guide matches what you see in the running UI - [ ] Decide whether to run the screenshot overhaul before or after merging this - [ ] **After squash-merging: run `node scripts/sync/realign-nightly.mjs release/v2026.720.0 --push` before the next nightly sync.** The squash severs `main`↔`nightly` ancestry; the realign (shipped in this PR) prevents add/add conflicts at the next nightly run. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: DevRel Gemini <[email protected]> Co-authored-by: Paperclip <[email protected]>
23 KiB
Maintenance
Operational notes for maintainers of the Paperclip docs site. Reader-facing instructions live in README.md.
For operational instructions on running the
/sync-docsskill (first-time setup, daily operation, release ceremony, troubleshooting), see runbook.md.
Repo layout
site/ # Static site shell + release builder
├── index.html # Main SPA (routing, rendering, TOC, search)
├── content.json # Section/page manifest — source of truth for sidebar & landing (titles, icons, descriptions, pages)
└── build-release.mjs # Produces a standalone bundle in .site/
docs/ # Markdown content only
├── guides/ # Quickstart, Day-to-Day, Org & Agents, Projects & Workflow, Power Features
├── how-to/ # Field-tested recipes (budgets, approvals, skills, deployment troubleshooting)
├── administration/ # Company settings, plugins, CLI auth, company config
├── reference/ # API, CLI, adapters, deploy, skills (the "what" reference tier)
│ ├── api/
│ ├── cli/
│ ├── adapters/
│ ├── deploy/
│ └── skills.md
└── user-guides/ # Long-form user guides + screenshots/{light,dark}
└── screenshots/
└── registry.json # Screenshot dependency tracking (see Screenshots)
scripts/
├── publish-gh-pages.sh # Builds and pushes to the gh-pages branch fallback
├── rewrite-links.mjs # Link-rewriting utility for builds
└── sync/ # /sync-docs skill config (anchor map, state template)
skills/ # Claude Code skills shipped with this repo
└── sync-docs/SKILL.md # The /sync-docs orchestrator
.sync-state.json # Per-branch sync state (committed)
wrangler.jsonc # Cloudflare Pages project config
.github/ISSUE_TEMPLATE/ # Support / bug / docs-feedback templates
Commands
# Build the static site into .site/ using the production custom-domain base path
npm run docs:build
# Build with auto base-path detection (useful when serving from the repo root locally)
npm run docs:build:auto
# Serve .site/ on http://localhost:4321/
npm run docs:serve
# Build and deploy to Cloudflare Pages
npm run docs:publish
# Build and push to the gh-pages branch fallback
npm run docs:publish:github
Base paths
build-release.mjs accepts --base-path <path|auto>:
- Production custom domain — use
/(the default indocs:buildanddocs:publish) forhttps://docs.paperclip.ing/. - GitHub Pages repo subpath preview — use
/paperclip-docs/vianpm run docs:build:github-subpath. - Local preview — use
auto(docs:build:auto). Paths resolve relative toindex.html, so the site works under any prefix (or none). - Self-hosting under a different prefix — pass an explicit path, e.g.
--base-path /docs/.
Mismatched base paths are the most common cause of 404s after a build — if asset URLs point to /paperclip-docs/... but you serve at /, everything breaks.
Adding or moving pages
- Create the Markdown file in the appropriate
docs/<section>/folder. - Register it in
site/content.jsonunder the correct section. Thefilefield is relative tosite/(typically"../docs/<section>/<page>.md"). - Page titles, section grouping, and sidebar order all derive from
content.json. There is no filesystem-based auto-discovery. - Slugs are derived from the file path —
user-guides/guides/foo.mdbecomes/#/foo, other sections keep their path.
Section icons
Landing cards and sidebar section headers render icons via Lucide. The UMD build is loaded from CDN in site/index.html.
To set or change a section's icon, edit its icon field in site/content.json:
{
"title": "CLI",
"icon": "terminal",
"desc": "…",
"pages": [ … ]
}
The value is any Lucide icon id in kebab-case (e.g. rocket, layout-dashboard, settings-2, braces). Browse lucide.dev/icons to find one. No code changes are required — app.js emits <i data-lucide="<name>"></i> placeholders and calls lucide.createIcons() after each nav render to swap them for inline SVG. If the name doesn't match a Lucide icon, the placeholder stays empty, so test after changing.
Screenshots
Store UI captures under docs/user-guides/screenshots/{light,dark}/<name>.png. The renderer automatically swaps the correct theme variant at runtime based on the user's current theme. Always provide both.
Registry
Every screenshot used in docs should have an entry in docs/user-guides/screenshots/registry.json so the sync skill can flag staleness when the underlying UI code changes:
{
"file": "screenshots/light/dashboard-issues.png",
"route": "/issues",
"viewport": "1440x900",
"theme": "light",
"captured_against": "v2026.318.0",
"captured_sha": "<parent commit sha at capture>",
"depends_on": [
"server/src/routes/issues.ts",
"ui/src/pages/Issues.tsx"
]
}
depends_on lists parent-repo paths whose changes likely invalidate this screenshot. The /sync-docs skill diffs those paths across each run window and writes any stale entries to SCREENSHOTS_PENDING.md. Recapture stale (or all) screenshots via the pipeline described in scripts/screenshots/README.md.
Per-article feedback links
Every article ends with "Suggest an edit" and "Report an issue" links. These are generated in loadPage() in site/index.html and point at:
github.com/paperclipai/paperclip-docs/edit/main/<path>— GitHub web editor.github.com/paperclipai/paperclip-docs/issues/new?template=03-docs-feedback.yml&...— prefilled docs-feedback issue.
If the repo is ever renamed or mirrored, update DOCS_REPO_SLUG / DOCS_REPO_BRANCH constants near the top of appendPageFeedback().
Issue templates
.github/ISSUE_TEMPLATE/ holds three templates:
01-support.yml— end-user support questions.02-bug.yml— bug reports against the docs site itself (not product bugs).03-docs-feedback.yml— referenced by the per-article "Report an issue" link. Thedocs_pagefield id must stay in sync with the URL param emitted inappendPageFeedback.
Publishing
Primary publishing uses Cloudflare Pages, matching the rest of the public Paperclip sites:
npm run docs:publish
That command builds .site/ and runs:
wrangler pages deploy .site --project-name paperclip-docs --branch main
The Cloudflare Pages project name is paperclip-docs; production deploys use branch main.
GitHub Pages fallback
scripts/publish-gh-pages.sh clones the repo into a tempdir, checks out (or creates) gh-pages, replaces its contents with a fresh build, commits, and pushes. It leaves the working tree untouched, so it's safe to run with uncommitted changes. A .nojekyll marker is added so GitHub Pages serves files whose names start with _.
By default, the publish script builds for https://docs.paperclip.ing/ and writes a CNAME file containing docs.paperclip.ing. Override with PAGES_BASE_PATH=/paperclip-docs/ PAGES_CUSTOM_DOMAIN= npm run docs:publish:github only when intentionally publishing a GitHub Pages subpath preview without the custom domain.
Sync workflow
These docs follow the parent paperclipai/paperclip repo automatically via the /sync-docs skill. The skill detects code-surface changes (CLI commands, env vars, REST routes, adapters, plugin SDK, schemas) and produces edits in our friendly-tutorial voice.
Our docs are not a translation of the parent's docs. The parent's code is the source of truth; its release notes and inline docs are reference data for understanding intent.
Architecture
Two diagrams summarise the sync workflow. The first shows the branch model — how the parent repo's releases and HEAD relate to this repo's main and nightly branches. The second shows the per-run flow of a single /sync-docs invocation.
Branch model
flowchart LR
subgraph parent["paperclipai/paperclip (parent)"]
direction TB
master["master HEAD<br/>(continuously updated)"]
T1["v2026.318.0"]
T2["v2026.319.0"]
T3["..."]
T4["v2026.512.0<br/>(latest release)"]
T1 -.-> T2 -.-> T3 -.-> T4 -.-> master
end
subgraph docs["paperclip-docs"]
direction TB
docsmain["main<br/>(docs.paperclip.ing — released)"]
nightly["nightly<br/>(preview URL — draft state)"]
nightly -- "release PR (squash-merged)<br/>(when parent tags)" --> docsmain
docsmain -- "realign-nightly.mjs<br/>(required after every release —<br/>squash severs ancestry)" --> nightly
docsmain -- "merge main → nightly<br/>(at every nightly run,<br/>absorbs hot-fix typos)" --> nightly
end
T4 -. "release-mode<br/>diff base" .-> docsmain
master -. "nightly-mode<br/>HEAD reference" .-> nightly
T1 -. "cumulative diff base<br/>(both modes)" .-> nightly
classDef parentStyle fill:#fef3c7,stroke:#d97706
classDef docsmainStyle fill:#dcfce7,stroke:#16a34a
classDef nightlyStyle fill:#dbeafe,stroke:#2563eb
class master,T1,T2,T3,T4 parentStyle
class docsmain docsmainStyle
class nightly nightlyStyle
Key invariant: base_release_tag in .sync-state.json only changes on a successful release merge to main — that's what keeps cumulative diffs stable.
Per-run flow
flowchart TD
Start(["/sync-docs invocation"]) --> Pre
subgraph Pre["Preconditions"]
direction TB
P1["gh auth OK"]
P2["Working tree clean"]
P3["JSON files parse:<br/>.sync-state, anchor-map, registry"]
P4["Resolve parent default branch<br/>gh api ... .default_branch → master"]
P5["Preflight non-glob watcher paths<br/>(404 → warn, don't fail)"]
P1 --> P2 --> P3 --> P4 --> P5
end
Pre --> ModeCheck{New parent<br/>release tag?}
ModeCheck -->|yes| RM["Release Mode<br/>checkout docs main"]
ModeCheck -->|no| NM["Nightly Mode<br/>checkout docs nightly<br/>merge main into nightly"]
RM --> Phase15
NM --> Phase15
subgraph Phase15["Phase 1.5 — Drift check (independent of window)"]
D1["node scripts/sync/check-drift.mjs<br/>--against master"]
D1 --> DR[("4 drift kinds:<br/>parent-path · cli-command<br/>env-var · rest-route")]
end
Phase15 --> Phase2
subgraph Phase2["Phase 2 — Cumulative diff (recursive bisection)"]
direction TB
CW["node scripts/sync/compare-window.mjs<br/>base_release_tag → target --json"]
CW --> Trunc{Response<br/>truncated?<br/>files=300 OR<br/>commits<total}
Trunc -->|yes| Split["Split at median SHA<br/>recurse on each half<br/>cache leaves"]
Split --> CW
Trunc -->|no| Merge["Union leaves<br/>status-merge rules:<br/>added+removed → drop<br/>added+modified → modified<br/>renamed wins"]
Merge --> DiffOut[("changed file list<br/>1,726 files · 22 leaves<br/>truncated=0")]
end
Phase2 --> Phase3
subgraph Phase3["Phase 3 — Build manifest"]
WatchLoop["For each of 12 watchers<br/>in anchor-map.json"]
WatchLoop --> Intersect["Intersect parent_paths<br/>globs vs changed files"]
Intersect --> Classify["Apply detect rule<br/>tier: auto-merge / PR / context-only"]
Classify --> ManOut[("manifest.yaml<br/>+ PENDING.md regenerated<br/>+ manifest_hash")]
end
Phase3 --> Phase35
subgraph Phase35["Phase 3.5 — Reconciliation"]
Hash{"manifest_hash ==<br/>last_applied_manifest_hash?"}
Hash -->|yes| Skip["No changes — skip to Phase 6"]
Hash -->|no| Delta["Compute delta:<br/>new entries vs<br/>DISAPPEARED entries"]
Delta --> ReconFlag["⚠ Disappeared = previously<br/>applied edit whose parent<br/>commit was reverted<br/>Surface for human review,<br/>never auto-undo"]
end
Phase35 --> Phase4
Phase4{"--dry-run?"} -->|yes| PrintExit["Print summary:<br/>manifest by tier<br/>+ drift + reconcile<br/>+ screenshot staleness<br/>EXIT"]
Phase4 -->|no| Phase5
subgraph Phase5["Phase 5 — Apply edits"]
direction TB
Entry["For each manifest entry"]
Entry --> Tier{Tier?}
Tier -->|auto-merge| Safety{"auto_merge_safety<br/>passes?<br/>not removed/renamed<br/>≤1 file · ≤50 lines"}
Safety -->|yes| Direct["Direct mechanical edit<br/>(append env-var row, etc.)"]
Safety -->|no| ToPR["Demote to PR tier"]
Tier -->|PR| Sub["Spawn subagent<br/>with voice rules +<br/>neighbour page for tone"]
ToPR --> Sub
Sub --> Return["Returns:<br/>files + nav_addition"]
Direct --> Collect["Orchestrator collects<br/>nav_additions"]
Return --> Collect
Collect --> ContentMerge["Single coordinated<br/>content.json edit<br/>(no parallel race)"]
end
Phase5 --> Phase6
subgraph Phase6["Phase 6 — Screenshot staleness"]
Reg["registry.json depends_on<br/>vs changed files"]
Reg --> StaleOut[("SCREENSHOTS_PENDING.md<br/>regenerated (not appended)")]
end
Phase6 --> Phase7
subgraph Phase7["Phase 7 — Verify & commit"]
Build["npm run docs:build"]
Build --> Check["npm run sync:check<br/>lint-links + verify-nav + check-drift"]
Check --> Result{Pass?}
Result -->|no| Fail["Roll back edits<br/>report failed entry"]
Result -->|yes| Commit["Commit:<br/>nightly auto-merge → nightly branch<br/>nightly PR-tier → nightly-draft/* PR<br/>release → release/vX.Y.Z PR to main"]
Commit --> StateBump["Update .sync-state.json<br/>last_applied_manifest_hash<br/>last_run_at, outcome"]
end
Phase7 --> Phase8
Phase8["Phase 8 — Print summary<br/>(branch, commits, PRs,<br/>drift, reconcile flags,<br/>stale screenshots)<br/>Ask: push? open PR?"] --> End(["End — user decides<br/>never push without asking"])
classDef helper fill:#e0e7ff,stroke:#4f46e5
classDef data fill:#fef3c7,stroke:#d97706
classDef warn fill:#fee2e2,stroke:#dc2626
class D1,CW,Build,Check helper
class DR,DiffOut,ManOut,StaleOut data
class ReconFlag,Fail warn
Reading the per-run flow:
- Blue/indigo boxes are deterministic Node helper scripts under
scripts/sync/— unit-tested, no LLM tokens consumed when they run, the orchestrator just invokes them. - Yellow cylinders are data artifacts:
.sync-state.json,anchor-map.json,registry.json,PENDING.md,SCREENSHOTS_PENDING.md.PENDING.mdandSCREENSHOTS_PENDING.mdare regenerated each run, never appended. - Red boxes are warning paths surfaced for human review — drift candidates and reconciliation flags are never auto-resolved. The skill is fundamentally additive; it does not delete pages or undo prose without explicit confirmation.
Branch model
| Branch | Tracks | Deploys to | Audience |
|---|---|---|---|
main |
Latest parent release tag (e.g. v2026.512.0) |
docs.paperclip.ing (production) |
Everyone — end users to devs |
nightly |
Parent's main HEAD |
Cloudflare Pages branch preview | Early adopters, contributors |
End users on the latest released paperclip must never see docs for unreleased features. That's why nightly drafts live on a separate branch and never touch main until a parent release tag drops.
Two modes
Both modes use cumulative diffs — always from base_release_tag in .sync-state.json, never incrementally from yesterday. This makes reverts auto-cancel (net-zero in the cumulative window) and lets nightly be safely regenerated.
Nightly mode — runs daily.
- Diffs
base_release_tag → parent default-branch HEAD(currentlymaster) viascripts/sync/compare-window.mjs. - Auto-commits trivial mechanical edits (e.g. appending a new env var to the env-vars reference) directly to
nightly. - Opens PRs against
nightlyfor judgment-call rewrites. - Updates
PENDING.mdso you can see the accumulating doc backlog.
Release mode — runs when a new parent release tag appears.
- Diffs
base_release_tag → new_tag. - Most of the work is already done by nightly drafts; this mode mainly verifies completeness, stamps
paperclip_versionfrontmatter, and opens a single PR:Release docs for paperclip vX.Y.Z(mergesnightly→main). - On merge to
main, tag this repodocs/vX.Y.Z, Cloudflare deploys live. - Immediately after the squash-merge, run
node scripts/sync/realign-nightly.mjs release/vX.Y.Z --push. The repo is squash-only, so the merge seversmain↔nightlyancestry; the realign fast-forwardsnightlyonto the shipped content and re-anchors the merge-base. Skipping it makes the next nightly run fail with add/add conflicts on every page drafted that cycle.
Running the skill
# Auto-detect mode (nightly unless a new parent tag exists)
/sync-docs
# Force a mode
/sync-docs --nightly
/sync-docs --release
# Triage only — no edits, no commits, no PRs
/sync-docs --dry-run
# Override starting point (useful after manual interventions)
/sync-docs --since v2026.318.0
# Release mode: produce one PR per intermediate tag (use for big gaps)
/sync-docs --release --batched
Recommended cadence: /loop 24h /sync-docs for nightly. Release mode is auto-detected on the next run after a parent tag drops.
Config files
| File | Purpose |
|---|---|
scripts/sync/anchor-map.json |
Maps parent-repo paths → docs paths. Edit when adding new doc sections or new product surfaces. |
scripts/sync/state.example.json |
Template for .sync-state.json. |
.sync-state.json |
Per-branch sync state (committed). Schema: branch_mode, base_release_tag, base_release_sha, last_seen_parent_sha, last_applied_manifest_hash. base_release_tag is the diff base for both modes — cumulative diff, only bumps on a successful release merge. |
docs/user-guides/screenshots/registry.json |
Screenshot dependency tracking — see Screenshots. |
Helper scripts
The full list with example invocations lives in Runbook → Helper scripts (full list). Two behaviours worth noting at the design layer:
sync:lint-linksskips external URLs, absolute SPA routes (/...), and template placeholders (${...},{id}) — anything that isn't a real filesystem path. Exit 1 on any broken link.sync:verify-navreports two classes of issue: dangling nav entries (incontent.jsonbut not on disk — error, exit 1) and orphan files (on disk but missing fromcontent.json— warning, exit 0). Pass--strictto fail on orphans too.
See skills/sync-docs/SKILL.md for the full operational playbook.
Gotcha: detect-renames.mjs cannot see cross-package moves
scripts/sync/detect-renames.mjs matches top-level directories under the watched roots. It cannot see a move that crosses a package boundary into a nested path, so it reports one as removed_dirs_no_match — which reads as "this surface was deleted, consider removing its doc page."
That failure mode is silent and it points the wrong way: toward deleting docs that should be kept. Always confirm a removed_dirs_no_match against the parent commit before acting on it.
The case that exposed this (parent eedc7dd, #9238) had three layers the helper could not report:
- The runtime moved —
packages/adapters/acpx-local/→packages/adapter-utils/src/acpx-engine/. Different package, nested path, so no match. - The identifier stayed registered on purpose —
server/src/adapters/registry.tskeeps a retiredacpx_localentry so stale DB rows fail loudly instead of falling back to theprocessadapter. Gone fromAGENT_ADAPTER_TYPES, still resolvable at runtime. - Existing rows were migrated —
packages/db/src/migrations/0136_acpx_default_engine_migration.sql.
How to check, before you touch a page:
# What actually happened to the directory?
gh api "repos/paperclipai/paperclip/commits?path=<removed-path>/src/index.ts&sha=master&per_page=3" \
-q '.[] | "\(.sha[0:9]) \(.commit.message | split("\n")[0])"'
# Did the basename reappear as a nested path elsewhere in the window?
node -e "require('/tmp/paperclip-sync/window.json').files
.filter(f => f.filename.includes('<basename>') && f.status === 'added')
.forEach(f => console.log(f.filename))"
# Is the identifier still registered anywhere?
gh api "search/code?q=<identifier>+repo:paperclipai/paperclip" -q '.items[].path'
When a user-facing surface is retired, keep the page and convert it to a stub. Do not delete it and do not duplicate its body forward under a new name — retired pages usually document config fields that a migration has already rewritten, so copying them propagates dead config. The stub should say what replaced the surface, link to the pages that now cover it, and describe the migration path. Keep the content.json entry and mark the title (e.g. ACPX Local (retired)) so someone who meets the old identifier in a runtime error can still find it by browsing.
docs/reference/adapters/acpx-local.md is the worked example. docs/reference/cli/commands.md is the same pattern for a page that split rather than retired — that one is intentionally orphaned from content.json, since its readers arrive by direct URL rather than by browsing.
Hot-fixes on released docs
If you spot a typo or broken example on main (released docs), fix it on main directly — it deploys to docs.paperclip.ing on merge. The sync skill merges main back into nightly at the start of every nightly run, so the fix isn't lost.
Screenshots in the sync flow
The skill flags stale screenshots based on registry.json depends_on paths and writes them to SCREENSHOTS_PENDING.md. Capture is now handled by the built-in screenshot pipeline — see scripts/screenshots/README.md for prerequisites and full details.
Two commands cover the common cases:
# Recapture stale (or all) screenshots against a fresh seeded instance
npm run screenshots:refresh
# Recapture every screenshot after a major UI/UX redesign
npm run screenshots:refresh:all
Both commands spin up an isolated local_trusted Paperclip instance on loopback, seed it with demo data, capture light + dark variants at 1440×900 @2x via Playwright, and stamp captured_sha / captured_against in registry.json. The output PNGs go into a PR for human review — never auto-pushed.
See Runbook → First-time setup for installing the skill locally and seeding .sync-state.json. See Runbook → Helper scripts (full list) for every sync:* script with example invocations.