Skip to content
Back to skills

Prototype Loop

ASecurity

Leveraged-prototype dev loop (`pulp loop`) — focus marker plus normal watch/rebuild loop, with AOT analyzer guidance and deferred ar-swap/PR-monitor playbook. Use when porting an existing UI/bundle to Pulp, doing visual/behavioral parity work, or batching upstream framework gap fixes.

  • 22 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added June 11, 2026
toolsrustgoc++shellbashreactnodegitapidatabase

Works with

  • claude code
  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned September 30, 2026

npx -y skills add danielraffel/pulp --skill prototype-loop --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Prototype Loop?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Prototype Loop
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/danielraffel-prototype-loop/badge)](https://www.skillsdirectory.com/skills/danielraffel-prototype-loop)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: prototype-loop
description: Leveraged-prototype dev loop (`pulp loop`) — focus marker plus normal watch/rebuild loop, with AOT analyzer guidance and deferred ar-swap/PR-monitor playbook. Use when porting an existing UI/bundle to Pulp, doing visual/behavioral parity work, or batching upstream framework gap fixes.
requires:
  tools:
    - gh
    - cmake
---

# Prototype Loop Skill

Codifies the leveraged-prototype dev loop for closing framework gaps during
high-feedback UI parity work.

## When to use this skill

Trigger this skill when the user is doing any of:

- **Porting a UI to Pulp** and wants tight visual-feedback iteration (Spectr's WebView editor → native via `@pulp/react` is the canonical example).
- **Gap-finding** — they suspect there are framework gaps and want to enumerate them via an AOT analyzer.
- **Multi-PR upstream coordination** — they're filing a batch of framework PRs and want a tight loop while upstream merges land.
- Hitting **slow cross-platform configure cost** (Skia/Dawn/threejs FetchContent) on changes that only need single-platform validation during iteration.

Do **not** trigger for: bugfixes, cross-platform refactors, or final-landing flows. Those want the cross-platform default.

## When NOT to use this skill

- Pre-merge / landing the consumer's PR — always exit focus mode (or run `shipyard pr` / `pulp pr`) before landing. The ship path validates cross-platform regardless, but exiting focus mode keeps subsequent local iteration honest.
- Bugfixes that touch platform-specific code — a cross-platform build is the cheapest correctness check for those.
- Refactors that span subsystems — single-platform configure can mask a build break on a sibling platform.

## The loop in one breath

```
analyze → file → prototype (ar-swap) → monitor → bump → ship
```

Each step has tooling. The CLI loop is available now; deeper archive-splice, issue-monitoring, and analyzer-lift work should land separately.

## Current surface

| Surface | Status |
|---------|--------|
| `pulp loop` CLI + skill + slash command + docs | available |
| `pulp-ar-swap.sh` ABI-checked archive splice | deferred |
| `pulp loop --watch-issues` PR-monitor | deferred |
| Lift `@pulp/css-adapt`, `pulp-css-analyze`, `extract-html-bundle` | deferred |

## Step 0 — Start a new worktree warm, not cold

A fresh worktree's first build is a full configure plus every compile and
link, even when the worktree you branched from built the same tree minutes
ago. On macOS/APFS, `pulp build --seed-build` (or `PULP_SEED_BUILD=1` in the
environment so every first build does it) runs
`tools/scripts/seed_build_dir.py` before the first configure. It clonefiles
the warm Ninja build dir of the sibling worktree closest to `HEAD` (shared
blocks, no data copied) and retargets it: text files that name the donor
path are rewritten, `.ninja_deps` is re-emitted, `.ninja_log` command hashes
are recomputed, binaries that embed the donor path are dropped, and unchanged
clean sources get the donor's mtimes. The first build is then only what
differs from the donor. Receipt: `build/.pulp-seed-receipt.json` (edges left
to build, path-tainted binaries dropped, timings).

What to know before trusting it:

- It only ever removes gratuitous work. Anything Ninja cannot prove up to
  date is rebuilt, and it reads the donor without writing it (the suite
  audits the donor's inodes and mtimes).
- Eligible donors are Ninja, built, same `CMAKE_BUILD_TYPE` and
  `PULP_BUILD_EXAMPLES`, same APFS volume, and not mid-build. Anything else
  is refused with exit 3 and nothing left behind, and `pulp build` configures
  from scratch as usual. Cross-volume never silently copies.
- A Release donor built through ccache with `base_dir` gains almost
  everything; a Debug donor (`-g`) embeds the path in every object and gains
  only the configure. Targets whose `-D` defines carry the source dir
  recompile regardless.
- `dirty_edges` in the receipt is Ninja's planned total from a dry run of a
  copy of `build.ninja`. A plain `ninja -n` stops at CMake's always-dirty
  glob check and reports 2, and counting its status lines undercounts,
  because a dry run skips the status line of edges that finish together.
  When `cmake_rerun_pending` is non-empty (a `CMakeLists.txt` differs from
  the donor's, e.g. a VERSION bump) CMake re-runs first and the count is a
  lower bound.
- `pulp build` in a fresh worktree has an empty diff, so its focused
  selector widens to `all`; on a seeded dir that is exactly the leftover
  edges, not a full build.

**Never start the worktree under `/tmp` or `$TMPDIR`.** Configure and
`governed-build.sh` refuse it (exit 3), because a temporary tree misses the
shared ccache and starts every build cold; create it under
`$PULP_WORKTREES_ROOT` or beside the primary checkout. A second `pulp build`
into a tree that is still building exits 75 and names the running build:
attach to that one instead of relaunching. `pulp loop` takes the same lock for
each rebuild, so a loop and a manual `pulp build` in one tree cannot race; a
rebuild that finds another build holding the tree reports 75 and the loop keeps
watching.

## Step 1 — AOT analyze the consumer's bundle

Run `pulp-css-analyze` over the consumer's pre-built React bundle. The output is a coverage report listing unmapped CSS props with occurrence counts.

Reference output shape:
```
Unmapped CSS props (8 total):
  fontFamily            14×   examples: src/Label.tsx:23, src/Header.tsx:8, ...
  textAlign             6×    examples: src/Caption.tsx:12, ...
  ...
```

The occurrence counts are how you prioritize — file the `14×` issue first.

## Step 2 — File framework issues with the right shape

Each gap deserves its own issue. Use this shape:

- **One-line title** — e.g. "Label.font_family_ accessor missing from public surface".
- **Occurrence count** — "Used in 14 places across the Spectr bundle (see analyzer report URL)".
- **Acceptance criteria** — concrete, testable. "`label.set_font_family("Inter")` followed by `label.font_family() == "Inter"` returns true".
- **Bridge-fn signature suggestion** — "`label.set_font_family(string)` mirroring `set_font()`".
- **Cross-link to the analyzer report** that surfaced it.

Filing 6 well-shaped issues with concrete signatures cuts upstream ramp-up dramatically. If you're going to file an umbrella + sub-issues, link the sub-issues from the umbrella's body so the dashboard view is coherent.

## Step 3 — Enter focus mode (`pulp loop`)

```bash
pulp loop                       # auto-detect host platform
pulp loop --platform=macos      # explicit override
pulp loop --status              # report current state
pulp loop --off                 # restore cross-platform mode
```

The CLI persists `[loop] focus_platform = "..."` in `~/.pulp/config.toml`. Subsequent invocations stay pinned until explicitly cleared.

The watch loop is also **focused on the working diff by default**: before every
rebuild it re-runs the affected-target selector (`pulp affected`, script
`tools/scripts/affected_targets.py`) and passes `cmake --build --target` only
the targets that own the diff plus their companion test programs
(`test_<stem>*.cpp`), following `add_dependencies` and CTest fixture edges. With
`--test` it runs only those tests via `ctest --tests-from-file`. A one-line
`.cpp` edit in `core/view` rebuilds `pulp-view-core` + `pulp-test-widgets` in
seconds instead of relinking ~1,400 programs. The banner it prints is the
contract:

```
FOCUSED: building 3/1708 targets affected by your diff - run 'pulp build --all' before opening a PR
```

- `pulp loop --all` (and `pulp dev --all`, `pulp build --all`) restores the full
  build; `--target` and `--test-filter` always win; `PULP_BUILD_FOCUS=0`
  disables focus for a shell.
- The selector falls back to `all` and says why for an empty diff, a
  build-system change, an unmapped C/C++ file, or a selection above ~40% of the
  target graph. Header edits focus only when a dependency database exists
  (`ninja -t deps` or Makefile `.o.d` files).
- The first focused run in an existing build dir configures once (~80 s) to
  record the CMake file-API codemodel it selects from.
- **Focused green is not landing green.** Pre-push and Shipyard still build
  `all`; run `pulp build --all && pulp test --all` before `shipyard pr`.

A fresh source-checkout configure leaves the example projects off (`-DPULP_BUILD_EXAMPLES=OFF`) and pins Ninja + Release, like `pulp build`. When the prototype lives under `examples/`, pass `pulp loop --examples` so the first configure (or a reconfigure of a tree that has examples off) includes it. An existing tree an older CLI left on Makefiles, Debug, or examples ON is migrated on the first `pulp loop`: its cache moves to `build/.pulp-pre-migration/`, one `Reconfiguring … (was: …)` line prints, and the tree configures fresh (a one-time full rebuild). A prototype under `examples/` must therefore pass `--examples` on every run, or the migration turns examples back off; `PULP_KEEP_BUILD_CONFIG=1` keeps a tree as it is.

`--no-watch` flips state and exits without entering the watch loop — this is what tests use, and it's also useful when you want the marker but plan to drive builds yourself.

## Step 4 — Local prototype via ar-swap

Pick the simplest gap from your filed issues. Build the framework patch in another worktree:

```bash
git worktree add ../pulp-fix-issue-X feat/fix-issue-X
cd ../pulp-fix-issue-X
cmake --build build --target pulp-view
```

The planned `pulp loop --ar-swap-from ../pulp-fix-issue-X` helper will splice the changed `.o` files into the pinned SDK's static archive *after* validating header/library ABI. The validator refuses on vtable mismatch — the failure mode where a consumer compiles against stale layout assumptions such as `Label::font_family_`.

Until that helper lands, do the ar-swap by hand:
1. Build the patched object file in the other worktree.
2. `nm -gU` the object — make sure exported symbols match what your consumer's compile expects.
3. `ar -r <pinned-sdk>/lib/libpulp-view.a <patched.o>` — splice.
4. Visually validate via `pulp-screenshot` or another explicit capture path.
5. **DELETE the local archive after validation.** Otherwise you'll forget it's spliced and ship a binary that doesn't match the upstream pin.

## Step 5 — Monitor upstream PR state flips

When automatic upstream monitoring is available:
```bash
pulp loop --watch-issues 924,927,931,932
```
will poll `gh pr list` for state flips on PRs referencing the named issues. It fires a notification when each PR transitions to `MERGED`.

Until then, run this in a side terminal:
```bash
watch -n 60 'gh pr list --state merged --search "924 OR 927 OR 931 OR 932" --json number,title,mergedAt'
```

## Step 6 — Bump SDK pin and validate cross-platform

After the upstream batch merges and auto-releases:

1. **Bump the consumer's SDK pin** in one shot (e.g. `0.52.0 → 0.56.0` if v0.53/v0.54/v0.55/v0.56 each came from one of the merged PRs). Update `pulp.toml` / `find_package(Pulp …)`.
2. **Run `pulp loop --off`** — restore cross-platform mode.
3. **Run `shipyard pr`** (or `pulp pr`) — full cross-platform validation gates the merge. This is the contract: focus mode for *iterating*, cross-platform for *landing*.

## Filing follow-up issues

When you discover a new framework gap *while* in focus mode, file it the same way as Step 2. Don't fix it locally and forget — the loop's discipline is upstream-first.

If you fixed something locally to keep iteration moving and the upstream issue isn't merged yet, leave a `// TODO(issue-NNN)` marker and a planning doc note in the consumer. The skill is "file framework issues *immediately*", not "fix and forget".

## Switching modes

- **Enter:** `pulp loop --platform=macos` → persists the focus marker and runs the normal project watch/rebuild loop.
- **Exit:** `pulp loop --off` → restores cross-platform mode.
- **Land:** `shipyard pr` (or `pulp pr`) → still runs full cross-platform validation before merge regardless of focus state.

The mode is *advisory* at the build layer today. The marker is read by tooling that wants to know "is the developer iterating or landing?", but it does not silently hide cross-platform breakage. Keep the marker semantically clean: "I am iterating, please don't surprise me with cross-platform configure cost."

## Common pitfalls

- **Forgetting to exit focus mode before landing** — `pulp loop --off` is one extra keystroke, but it's how you preserve the "single-platform for iterating, cross-platform for landing" separation. The ship path validates regardless, but the marker should match reality.
- **Locally hacking around a framework gap instead of filing it** — easy to fall into, especially when you're in flow. The skill prompts upstream issue-filing as the preferred path; resist the local-fix temptation.
- **Drifting past merged upstream PRs** — the planned `--watch-issues` monitor is the structural answer. Until then, set a 5-minute timer or pin the `gh pr list` watch in a side terminal.
- **ar-swap leaving SDK in inconsistent state** — header vs `.a` vtable mismatch is the trap. The planned helper refuses on mismatch; until then, `nm -gU` the patched object before splicing and delete the local archive after validation.
- **Multiple worktrees on different focus platforms** — the marker is per-`PULP_HOME`, so two worktrees pointing at the same `~/.pulp/config.toml` share the marker. If you're working on macOS in one worktree and want a Linux focus check in another, use `PULP_HOME=/tmp/pulp-linux pulp loop --platform=linux`.

## Slash command

The Claude Code slash command lives at [`.claude/commands/prototype-loop.md`](../../.claude/commands/prototype-loop.md). It asks the user to confirm the focus platform, then orchestrates the loop.

## Reference

- Planning issue: leveraged-prototype dev loop.
- Validation example: Spectr native React editor coverage using focused Pulp framework-gap issues.
- Coverage report: spectr `feature/native-react-editor` branch, `planning/spectr-style-coverage-report.md`
- Companion docs: [`docs/guides/focus-mode.md`](../../docs/guides/focus-mode.md)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…