git clone <repo>
cd iterator
# No npm install needed — the server uses only Node built-ins (Node ≥ 18).Load it into a Claude Code session with --plugin-dir:
claude --plugin-dir .Then, in any git repo, run /iterator for the dashboard, or /iterator-plan
to start the flow (plan → feature → implement → review). (For a persistent
install, use /plugin marketplace add <path> + /plugin install iterator —
the repo ships a .claude-plugin/marketplace.json. pi installs the repo as a
package: pi -e ., which also loads extensions/iterator.js for friendly
/iterator… commands.)
npm test # node:test — no dependenciesThe suite unit-tests lib/ui.mjs (embed, escHtml, renderPage) and
boots the shared UI server on ephemeral ports to exercise the full
round-trip: view dispatch per step, page serve, /submit → stdout, the
Host-header rejection, the cancel grace period, the single-instance takeover,
and signal handling. Tests set ITERATOR_NO_OPEN=1 (no browser),
ITERATOR_CANCEL_GRACE_MS (short grace), and a private ITERATOR_REGISTRY
per spawn — all work outside tests too.
The shared server reads a JSON payload from stdin, picks the view from its
step field, and opens the browser. Sample payloads are wired up as npm
scripts:
npm run preview:hub # /iterator dashboard (control plane)
npm run preview:plan # /iterator-plan plan-review view
npm run preview:feature # /iterator-feature feature-plan view (graph, cards, split/merge)
npm run preview:review # /iterator-review feature-grouped diff review
npm run preview:test # /iterator-test test-plan view
npm run preview:knowledge # /iterator-knowledge knowledge plane dashboard
npm run preview:memory-review # knowledge skills memory card reviewEach opens http://127.0.0.1:7777/ (watch stderr for the real URL if a
foreign process holds the port). A preview replaces any iterator server that
is still running — that is the single-instance takeover working as designed.
/iterator-implement has no view of its own — it drives the review view in
"mode": "commit".
iterator/
├── .claude-plugin/plugin.json # manifest (name: iterator; skills auto-discovered)
├── extensions/iterator.js # pi extension: /iterator… commands → /skill:iterator…
├── lib/ # SOURCE OF TRUTH — synced into skills/iterator/lib/ (npm run sync)
│ ├── app.mjs # control plane: view dispatch, one-command gather, onSubmit hooks
│ ├── server.mjs # shared HTTP server: stdin→JSON, /submit + /cancel, takeover, timeout
│ ├── gather.mjs # deterministic state gathering (--step …)
│ ├── write.mjs # deterministic bundle writer (--schema lists op payload shapes)
│ ├── git.mjs / bundle.mjs # git helpers; frontmatter/index/log primitives + validation
│ ├── ui.mjs # shared page shell + "ink & ember" design tokens
│ └── views/ # one view module per step: hub, plan, feature, test, review,
│ # knowledge, memory-review
├── skills/
│ ├── iterator/ # hub skill — thin shims (server/gather/write) + bundled lib/ + PI.md
│ ├── iterator-plan/ # SKILL.md (logic only) + templates/format.md
│ ├── iterator-feature/ # SKILL.md (logic only)
│ ├── iterator-implement/ # SKILL.md (logic only; uses the review view in commit mode)
│ ├── iterator-design/ # SKILL.md (logic only; design params + UI quality rules)
│ ├── iterator-review/ # SKILL.md (logic only)
│ ├── iterator-test/ # SKILL.md (logic only)
│ └── iterator-knowledge, iterator-init, … # knowledge skills + shared PROTOCOL.md
├── templates/format.md # SOURCE OF TRUTH — bundle schema, copied into every memory/ bundle
├── scripts/sync.mjs # copies lib/ (+views) into the hub skill (npm run sync)
├── docs/OKF_SPEC.md # Open Knowledge Format v0.1 spec
├── test/ # node:test suite (npm test, no dependencies)
├── .github/workflows/ci.yml # runs the tests on push/PR
├── ARCHITECTURE.md
└── README.md
The UI lives only in the hub skill: skills/iterator/ carries thin shims
(server.mjs, gather.mjs, write.mjs) plus the bundled cores and views
(skills/iterator/lib/); the step skills are logic-only and invoke the hub's
scripts as <skill-dir>/../iterator/<name>.mjs, so the skill folders must be
installed together. The repo-root lib/ and templates/ are the source of
truth: edit those, then run npm run sync to refresh the bundled copies.
test/sync.test.mjs fails if they drift.
Run npm run hooks:install once after cloning — it points core.hooksPath
at scripts/githooks/, whose pre-commit hook runs the sync and stages the
refreshed copies, so drift can never be committed. The sync test remains the
CI backstop for clones that skip the hook.
skills/<name>/SKILL.md— instructs Claude how to run the skill: the steps, the payload to pipe into the shared server, and how to handle the server's output (including how it reads/writes thememory/bundle).lib/app.mjs— the control plane (invoked through theskills/iterator/server.mjsshim): parses the stdin payload, gathers in-process when it is the one-command form ({"gather":true,"step":…}), picks the view module frompayload.step, attaches per-step cancel/timeoutreportstrings, dispatches action results to their owningskill, and callsserve().lib/views/<step>.mjs— a thin step-specific view: exportsrender(data)supplying a body renderer + step-specific browser JS. All shared chrome (header, theme, CSS variables,esc,mdToHtml, thepost()submit helper, the Accept ↔ Send review flip) comes fromlib/ui.mjsviarenderPage().skills/iterator/lib/— the hub skill's private copy of the shell and views, generated bynpm run sync. Never edit it directly; edit the repo-rootlib/instead.
Header, theme toggle, CSS variables, the markdown renderer, and the
submit/cancel/timeout/takeover behavior all live in the repo-root lib/ui.mjs
and lib/server.mjs — edit them there, run npm run sync, and every view
changes together. Step-specific markup, CSS, and JS live in that step's
lib/views/<step>.mjs. The shared embed() escapes < so payload data
containing </script> can't break the page.
The port defaults to 7777; override it with the ITERATOR_PORT environment
variable (the default lives once in lib/server.mjs). The port is stable
across runs: a lingering iterator server is shut down and replaced
(single-instance takeover; ITERATOR_NO_TAKEOVER=1 disables it). Only a
foreign process on the port makes the server walk up — the real URL is always
printed to stderr. Keep the fixed port in sync with any sandbox port forward
(pi-docker-sandbox-setup publishes 7777:7777).
- Claude runs the skill's steps (bundle/git state comes from
gather.mjs— never read by hand). - Claude pipes a payload to the hub's
server.mjs— usually the one-command form{"gather":true,"step":"<step>","extra":{…}}(the server gathers the base payload itself and merges the agent-authoredextra); nothing is written to/tmp. - The server replaces any lingering iterator server, serves the page, opens the browser, and blocks until submit.
- Claude reads the stdout JSON and records results through
write.mjs(--schema <op>prints any op's payload shape), then re-runs for the next round.
- Create
skills/<name>/SKILL.mdwith YAML frontmatter (name,description). - If it needs a UI, add
lib/views/<step>.mjsexportingrender(data), register it in theVIEWSmap oflib/app.mjs(and the COPIES list inscripts/sync.mjs), and runnpm run sync. The SKILL.md then pipes its payload (with the newstep) into<skill-dir>/../iterator/server.mjs. - Restart the session with
claude --plugin-dir .(or reinstall via the marketplace) to pick up the new skill.