Preserve installed versions and discovered auth, i18n, theme, and transport decisions.
Add trusted-request, agent-ready public-surface, async-state, and layered testing contracts.
kache is content-addressed and path/target-dir-independent, so its store warms itself across worktrees
and persists across runs — the first worktree to compile a crate populates it and every other worktree
and later run hardlinks it (measured ~97% cross-worktree hits, 51 GiB persistent store). A pre-warm on a
hot store is just a full workspace build that's all hits — dead weight. Remove the automatic kache
pre-warm from the warm step; document the one case it earns its keep (a genuinely COLD store — fresh
machine, kache purge, or Cargo.lock/toolchain bump — with a wide first build layer) as a trip-wire.
The no-kache path is unchanged: it still builds ROOT/target so worktrees CoW-seed it. Wording clarified
so a kache repo is never described as cold-building (kache still wraps via .cargo/config.toml).
Follow-up to v1.16.0: the kache onboarding missed four stale sccache mentions that still
described the warm step as using sccache — the warmWorktree default comment, the warm-provisioning
rationale ("a shared sccache compiler cache"), the seed-block verification note, and the resume
near-noop note. This repo uses kache (a path-independent rustc-wrapper wired in .cargo/config.toml,
~97% cross-worktree hits); sccache was removed and is not used. All four corrected to kache.
No behavior change — the kache priming + no-target/-clone path is unchanged.
Restyle the description and when_to_use frontmatter of the whole skill catalog to the
skills-engineering-autonomous pattern, so Claude Code and Codex route to the right skill:
- description: capability + em-dash/colon differentiator (key invariant in CAPS) -> concrete sequenced
mechanism -> explicit fail-closed/honesty clause -> a terse trailing "Use ..." (atomic skills) or a
phase-chain/"Composes ..." closer (composites).
- when_to_use: "Use when <positives + examples>. Do NOT use <negatives> -> redirect to the sibling
skill by name; <crisp principle>." + explicit-user-only/confirm caveat where the skill mutates or
spawns agents.
Frontmatter-only: skill bodies are unchanged. Grounded each rewrite in the skill's actual behavior.
(ship-playbook's restyle rode with its v1.16.0 commit.)
Resume/integration hardening (post-mortem of wf_faec74a9-170, which lost 27 of 74 tasks on resume):
- Derive integration state from git on resume: a task whose slice commit is on the working branch is
integrated (and "already applied" re-merges count as integrated, never dev_done), so dependents no
longer cascade-block and vanish.
- A dead/empty configured reviewer (codex/kiro) falls back to a real native review, never a false block;
a startup health-probe downgrades a broken harness to native once, and the return surfaces the downgrade.
- Treat nextest "no tests to run" (exit 4 / zero-match filter) as a plan/filter defect, not a slice failure.
- Park unmerged wf/build/* branches under refs/ship/parked/ before cleanup so gc can't reap recovery commits.
- Re-target a drifted base on resume; recognize drift/duplicate merge hunks as no-ops.
- Resume carries full args via wf-status.mjs --resume (canonical), not resumeFromRunId (which drops args).
Rust worktree caching: kache-first (a path-independent, hardlink-based rustc-wrapper with proven
cross-worktree hits, wired via .cargo/config.toml) with target/ CoW-seed as the no-kache fallback;
sccache stays removed (0% cross-worktree hits). Both build briefs, the seed script, and the warm step
prime/detect kache; SKILL.md sccache mentions updated.
Also restyles ship-playbook's own description + when_to_use to the routing-first frontmatter pattern.
Strips the `paths:` frontmatter allow-list from all 7 path-gated framework
skills (laravel, laravel-filament, docker, nodejs, nestjs, nextjs, rust). They
were staying dormant until a matching file was touched and then auto-suggesting
— which misfired across stacks (e.g. the nodejs skill waking on package.json /
Dockerfile in a PHP repo). Without `paths:` they behave as ordinary on-demand
skills. Minor version bump on each; no other frontmatter or body changes.
sccache --show-stats on a real run: 6 GB / 7603 files written, ZERO Rust
read-hits ever. Root cause is fundamental to the isolated-worktree design — each
worktree has a distinct target-dir path, and cargo bakes per-worktree absolute
paths into every rustc call (--extern/-L paths + path-dependent rlib contents),
so each crate is written under one cache key and looked up under another. Never a
hit; every rustc just detours through sccache (hash → miss → compile → write) for
zero payoff, adding overhead to a build that already cold-compiles everything.
Removes all Rust sccache machinery: the seed script's .cargo/config.toml wiring,
the warm-prime priming (size config file + daemon restart + throwaway build), and
the engineer-brief instructions. Rust worktrees now cold-build with plain cargo —
no seed, no prime, no config, no overhead. Node/PHP/Pods CoW seeding is untouched.
A target/ clone is also unsafe (cargo fingerprints embed abs paths → a moved
target rebuilds anyway). Any future Rust cache must first PROVE a non-zero hit
rate on a real run (candidate: --remap-path-prefix to stabilise paths) — not
shipped on faith. Verified: seed passes bash -n and writes no .cargo/config.toml.
v1.13.0 already wrote `[build] incremental = false` (which per cargo docs
overrides all profiles), but the CARGO_INCREMENTAL env var outranks it — so a
`dev-fast.sh`/validate that exports CARGO_INCREMENTAL=1 for its local fast loop
silently re-enables incremental, and sccache skips incremental units → the ~30
first-party workspace crates cold-compile in every worktree (observed: sccache
hitting only unchanged third-party deps, ~56%).
Seed's .cargo/config.toml now also writes:
[env]
CARGO_INCREMENTAL = { value = "0", force = true }
force=true neutralizes an ambient CARGO_INCREMENTAL, so incremental is off
regardless of what the repo's build script exports → sccache caches the
workspace crates too (2nd worktree onward hits instead of recompiling).
Precedence (cargo docs): CARGO_INCREMENTAL env > build.incremental > profile.
Verified: rendered seed passes bash -n; emitted .cargo/config.toml parses as
valid TOML with both [build] incremental=false and the forced [env] entry.
Live diagnosis on a real run: 7 of 8 rustc procs were UNWRAPPED — each worktree
engineer cold-compiled arrow/datafusion/sqlparser from scratch, and sccache
served only the warm prime's compiles. Root cause: the seed script's "Rust"
section was a comment that did nothing; it relied on the engineer brief's
`export RUSTC_WRAPPER=sccache`, but env does not survive across the agent's
separate shells / a fresh validate shell, so the real cargo/dev-fast.sh called
plain rustc. The v1.11–1.12 cache-size work was moot while the wrapper was bypassed.
Fix (env-independent, per the layer that actually reaches cargo):
- Seed script (runs in every worktree) now writes a worktree-local
.cargo/config.toml = `[build] rustc-wrapper="sccache"\nincremental=false`,
git-excluded via info/exclude so it never enters the engineer's commits,
created only-if-absent so a tracked .cargo/config is never edited. cargo reads
rustc-wrapper on EVERY invocation — dev-fast.sh, raw cargo, build.rs — no env.
- Engineer brief: stop telling engineers to `export` it (that was the bug);
rely on the config, inline-prefix if forcing env, and verify sccache
--show-stats climbs.
Verified: rendered seed script passes `bash -n`; run in a throwaway git repo it
produces valid TOML ([build] rustc-wrapper/incremental) and git-excludes the file.
The prior 50G cap silently reverted to sccache's 10G default. sccache reads
SCCACHE_CACHE_SIZE only when the SERVER starts; a bare `export` is ignored by an
already-running (or auto-restarted) daemon, and the single shared daemon is
almost always already up at 10G (stale server, warmWorktree off, parallel
engineers racing the first `cargo`, or an idle-timeout restart). Only the prime
step restarted the server, so only it applied the cap — the engineer path just
exported it into the void, then LRU-evicted the big dep graph mid-build.
Fix (warm prime, workflow-template.js):
- Persist the size in sccache's CONFIG FILE ([cache.disk] size = 85899345920 =
80 GiB), which is read on EVERY server start (manual, auto, post-idle-timeout)
regardless of which shell triggers it. Written only if absent, so an existing
sccache config is never clobbered. OS-aware path (Linux ~/.config, macOS
~/Library/Application Support/Mozilla.sccache).
- SCCACHE_IDLE_TIMEOUT=0 on the primed daemon so it survives the LLM-bound gaps.
- Raise cap 50G -> 80G in both the prime and the engineer build-shell export;
the engineer export is now belt-and-suspenders since the config file is
authoritative.
This is a run-time instruction to the build agent (writes the config on the
build machine), not a change to any user's global config.
The status file previously carried only file-level createdAt/updatedAt, so you
could see WHAT state each phase/task was in but not WHEN it got there. Adds
per-entry timestamps on every phase and task:
- workflow-template.js: the statusStep() jq merge now stamps startedAt (first
write, preserved across later merges via //) and updatedAt (rewritten every
write) onto only the phases/tasks named in each patch, alongside the existing
file-level updatedAt. Time is computed at write time by jq `now` (the Workflow
sandbox has no clock). Deep-merge + openRegister array-replace semantics are
unchanged; still-pending entries stay timestamp-free until first change.
- wf-status.mjs: the --write journal backfill now reconstructs per-task
startedAt/updatedAt from agent-transcript timestamps, matching the live shape.
- status-tracking.md: schema example + new Timestamps subsection.
Now phases.build.startedAt = when build began (updatedAt - startedAt = elapsed),
and a task's updatedAt = when it entered its current status (e.g. became blocked).
The v1.10.0/.1 feature shipped without user-facing docs. Adds:
- SKILL.md: warmWorktree in the Workflow args list + comment, and a Build-phase
paragraph on fast worktree provisioning (CoW-seed node_modules/vendor/Pods,
Rust sccache, frozen-install fallback, warmWorktree:false = plain install).
- README.md: the ship-playbook section now describes fast worktree provisioning.
Three corrections to the v1.10.0 warm/seed step:
- Gitignore is now surgical: .ulpi/.gitignore gets a `seed-worktree.sh` line
(appended if the file exists, else created with just that line) instead of
`*`, which would have hidden the plans/reviews/status files the workflow
writes under .ulpi and that you may commit.
- Raise the sccache cap to 50G (and stop/start the server so it takes effect):
the 10G default LRU-evicts a large dep graph (datafusion+arrow+candle) mid-run
and silently kills the hit rate. The cap is also set in the engineer's Rust
export so a cargo-autostarted server comes back at 50G, not 10G.
- Warm primes sccache into a THROWAWAY CARGO_TARGET_DIR (mktemp -d), not
${ROOT}/target — sccache's cache is target-dir-independent, so the shared
cache is populated without clobbering the local incremental fast-loop cargo
(e.g. dev-fast.sh) relies on. Also retires the torn-clone race.
Every build/fix engineer runs in an isolation:'worktree' checkout with empty
deps/artifacts, so each cold-installs or cold-compiles from scratch — per lane,
per layer, and again on resume. This seeds each worktree from the primary
checkout instead, per stack, and shares Rust compilation via sccache.
- One tested seed script (written to .ulpi/ by an early warm step, gitignored):
CoW-clones node_modules / vendor / Pods from ROOT when the lockfile is unchanged
(clonefile/reflink — instant same-volume), else a frozen --prefer-offline
install. Never a slow copy: the macOS path gates on same device id (cp -c
silently full-copies cross-volume), the Linux path uses --reflink=always
(errors instead of a silent fallback).
- Rust is sccache-only — NO target/ clone (redundant with sccache, multi-GB×N
disk, torn-clone risk). Engineers export RUSTC_WRAPPER=sccache
CARGO_INCREMENTAL=0 in the build shell (raw cargo and wrapper scripts inherit
it); the warm step primes sccache from ROOT before the fan-out.
- Warm starts right after preflight, overlapping the LLM-bound plan/plan-review
phases (~0 added wall-clock on fresh runs; a harmless near-noop on resume).
- Default on; CFG.warmWorktree=false restores today's plain per-worktree install
verbatim. Go left untouched (GOCACHE/GOMODCACHE already global).
Plan review's fix rounds edit the plan FILES on disk (planFixBrief: .json
source of truth, re-render .md) but return only FIX_RESULT {applied,notes} —
the in-memory `plan` object is never updated. buildPlan() reads plan.tasks
directly and never re-reads the files, so when a fix round runs the build
walks the PRE-FIX plan: founder-review fixes to writeScope / dependsOn /
layers / agent / validate silently don't reach the build.
Fix: planReviewLoop now reports whether it applied a fix; after any review
that did, reload the plan from disk (validate; keep the pre-fix object if the
reload fails to parse) so the build walks the fixed DAG. No-op when the review
is clean (no fix → no reload).
Extends the launch-* family to X (Twitter) and LinkedIn — own-audience social
launches — grounded in current sources and reviewed across multiple adversarial
rounds (9 findings fixed, 0 blockers/highs; facts independently re-confirmed clean).
New skills:
- launch-x — X launch orchestrator: writes .ulpi/launch/x/ (POST thread, PLAN
golden-hour runbook, OUTREACH, CHECKLIST, analytics). The link goes in a REPLY,
not the main tweet (link-in-body reach penalty); golden-hour replies are the top
lever (X's open-source code: a reply the author replies back to = 75.0, the
highest positive signal); X Premium is ~table-stakes for reach (Buffer's 18.8M-
post study, third-party). Phoenix (xAI, Jan 2026) replaced the heavy-ranker, so
2026 weights are labeled non-official.
- launch-linkedin — LinkedIn launch orchestrator: writes .ulpi/launch/linkedin/.
Problem-first hook (survives the ~140-char mobile "see more" fold), link in the
FIRST COMMENT (it's the preview card that hurts, and the penalty is contested/
smaller in 2026 — don't over-engineer), dwell-time-first (an officially confirmed
signal). Sanctioned team amplification ("Notify Employees") vs banned engagement
pods + engagement-bait (both official violations).
Unlike Product Hunt / Hacker News, these are own-audience posts with NO vote-
manipulation rule — you MAY ask your audience and team to genuinely engage. The
compliance work shifts to platform-penalized anti-patterns: engagement-bait,
bought/coordinated engagement / pods, and the link-in-body reach penalty.
Shared: launch-copy gains x + linkedin asset profiles; launch-outreach gains
x + linkedin engagement-allowed compliance modes + the engagement-bait scan.
README adds rows + sections for both. Each composes launch-copy / launch-outreach
/ launch-analytics with graceful fallback.
Adds a launch-skill family (one skill per platform + a shared core), grounded in
verified current sources and reviewed across 4 adversarial rounds (~43 findings
fixed, 0 blockers/highs; compliance + facts independently re-confirmed clean).
New skills:
- launch-product-hunt — PH platform orchestrator: grounds the product, writes a
paste-ready package to .ulpi/launch/product-hunt/ (LISTING, PLAN with an
hour-by-hour Pacific runbook, OUTREACH, CHECKLIST gate). Verified PH specs
(tagline 60, description 500, gallery 1270x760, topics <=3, four newsletters,
Golden Kitty -> Orbit, Coming Soon discontinued, re-launch 6mo + significant
update); official vs third-party labeled.
- launch-hacker-news — Show HN platform skill: writes POST/PLAN/CHECKLIST to
.ulpi/launch/hacker-news/. Grounded verbatim in showhn.html / newsguidelines /
newsfaq / dang; ranking formula labeled non-official. Forbids ALL vote
mobilization (stricter than PH); ready-to-ship gate (no signup wall, survives
the hug of death); technical/no-hype voice.
- launch-copy — shared copywriting engine; caller passes an asset profile
(assets/limits/voice/compliance); drafts A/B/C/D angle versions grounded in the
real product.
- launch-outreach — shared outreach; per-platform compliance mode (PH waves OK,
HN no mobilization); paste-ready, scan-clean message library.
- launch-analytics — shared UTM + GA4 conversion tracking.
Architecture: platforms compose the three shared skills with graceful fallback
(install-hint + built-in fallback, never hard-fail); one shared product brief at
.ulpi/launch/positioning.md, written once and reused. Compliance-first throughout
— coaches only compliant promotion, a banned-phrase scan gates every message,
refuses vote manipulation.
README: adds table rows + full sections for all five skills.
Replaces the per-LAYER batch build with a PER-TASK PIPELINE and fixes the
whole-suite-validate false-block that ground clean slices to death. Hardened
across ~16 adversarial verify→fix rounds (each round: find → independently
confirm; the final round confirmed 0).
Per-task pipeline (build-loop.md, workflow-template.js buildPlan/runTask):
- each task is its own unit: build → integrate → review → fix, run CONCURRENTLY
within a DAG layer. A task's review fires the moment IT integrates (not batched
after the whole layer), and fix loops run in parallel across tasks — only the
git merge serializes, via a single-slot merge lock. Layers stay barriered (DAG).
- integrate is MERGE-ONLY: no per-task whole-workspace VALIDATE_ALL. The old
per-task `pnpm -r test` turned an OTHER task's failure (half-migrated tree) into
a per-task blocker, looping a CLEAN-reviewed slice to MAX_FIX then false-blocking
it. Whole-tree validation now runs ONCE at the final validate gate.
- merge CONFLICTS are resolved (combine both sides, validate BEFORE commit, abort
only if truly unresolvable), not bailed. Merge is idempotent under retry
(clears stale MERGE_HEAD; merge-base ancestry short-circuit; branch-gone fails
CLOSED → dev_done/rebuild, never falsely 'integrated').
Fail-closed gates: a died/absent/empty reviewer (native/codex/kiro gateNotRun), a
died go-live audit, a blocking VERDICT with no finding, or a RED final validate all
keep openRegister non-empty → never a false converged. reviewOnce surfaces verdict;
per-task/impl/audit each guard the blocking-verdict-with-empty-findings case.
Durable resume + modes:
- wf-status.mjs --resume <id> emits the exact Workflow() relaunch from the status
file (planPath + checkpointResume, no resumeFromRunId, no args to hand-assemble);
the skill stores launchArgs at launch. SKILL.md Step 0 makes new-run vs resume
first-class.
- LAST STEP: a converged (clean) run archives the delivered plan to
.ulpi/plans/done/ (planArchived; skipped on needs_fix / external plans; non-fatal).
build-loop.md / playbook-state.md / SKILL.md / README updated; helper + template
syntax-checked; pure helpers unit-tested.
Hardened the build loop across nine adversarial verification passes (12→12→7→1→
4→4→3→3→0 confirmed findings; the false-clean invariant was independently
confirmed closed before shipping).
Checkpoint resume (durable, not cache-based):
- buildPlan reads the status file (readCheckpoints, before the plan-done write so
it isn't clobbered) and SKIPS every task already done — 'passed' (skip engineer
+ review) or on-branch (integrated/reviewing/fixing/blocked → skip engineer,
re-review). Independent of the runtime agent cache, which any template edit busts.
- SKILL.md: on resume REUSE the prior status file; do not overwrite it.
DAG dependency gate (build a task only when its deps are really on the branch):
- per-task dependsOn in the PLAN schema; planBrief requires complete deps +
topological layers + slice-scoped validate + merging inseparable tasks;
validatePlan rejects mis-ordered/cyclic/duplicate/omitted-task/unknown-dep plans
(and derives topological layers when none are given).
- runtime gate: a fresh task builds only once every dependsOn is integrated on
WORKING_BRANCH; a missing dep ⇒ dep_blocked (root-cause attribution, cascades).
Integrate matched by branch/id (id-agnostic), not a TASK-NNN regex.
Slice-scoped review (no false end-state blocks) + fail-closed gates:
- reviewer judges only the task's writeScope vs its acceptance; fix loop acts only
on in-scope blockers; fileInScope handles globs/abs-paths/empty-scope.
- a configured gate that did NOT run is NEVER clean: died/absent/empty reviewer
(native/codex/kiro gateNotRun), died go-live audit, and a RED FINAL workspace
validate (resume-safe, checks out WORKING_BRANCH) each keep openRegister
non-empty. Returns planReviewRan/implReviewRan/auditRan/workspaceValidatePassed/
endStateUngated/blockedTaskCount so the skill reports honestly. No uncaught throw
crashes the run (build/review/integrate/audit/plan all fail-soft).
Live status: status-writer agents per phase + DAG layer; durable .ulpi/workflows/
<id>.json; new statuses dep_blocked/dev_done. build-loop.md, status-tracking.md,
SKILL.md (rules 11-12 + Phase-2/3), README updated. Template syntax-checked; pure
helpers unit-tested.
reconstruct() already attributes reviews/fixes to tasks via the agent transcript
and reports latest-verdict-per-task (v1.6.2). This adds the last piece: surface
WHICH task each running agent is on. A started-with-no-result agent → its prompt
names the role+task → header shows "1 running NOW → fix:TASK-035#1" (and review:/
build:/integrate:…), plus --json.runningNow. No more bare "1 running".
The journal's review/fix results carry no task id, so the result-shape pass
couldn't tell "integrated" from "integrated but review-blocked" or see
review-pending. But each agent's transcript (agent-<id>.jsonl) starts with its
prompt, which names the role + task ("READ-ONLY review of TASK-032", "Fix
TASK-032 …", "integrate … wf/build/TASK-007"); agentId joins it to the journal
result + whether it finished.
- reconstruct(): join prompt(role+task) × result × started → true per-task status
pending/building/dev_done/integrated/reviewing/fixing/passed/blocked/dev_failed,
incl. review-PENDING and fixing. Reads only each transcript's first line; ~0.3s
over ~280 agents.
- render / --json / --write now use a DISJOINT status partition (sums to total)
with an onBranch rollup; --write stamps the true per-task status into the file.
- Verified on the live 75-task run: 13 passed, 24 review-blocked, 1 reviewing,
14 dev-failed, 23 not started (was reported flatly as "38 integrated").
status-tracking.md documents the prompt-join. Helper syntax-checked.
wf-status.mjs scoped runs to the current project by slugifying cwd with only
'/' → '-', but Claude's project dir slug replaces EVERY non-alphanumeric char
(so '_' and '.' too): /Users/x/work_cip/ulpi-v6 → -Users-x-work-cip-ulpi-v6.
Any path with an underscore/dot never matched its own project, so the default
(no --all) reported "no workflow runs found for this project". Slugify cwd with
/[^a-zA-Z0-9]/g → '-' to match. Verified against the real ulpi-v6 run dir.
A build task can fail because its acceptance/validate need state owned by a
not-yet-integrated task (e.g. TASK-053 needs the TASK-015 catalog rows + the
TASK-054 registry test). The build integrates layer-by-layer with a barrier, so
the real cause is a PLAN defect: the task was placed at/before its dependencies.
Fix it by following the DAG, not by retrying around it at runtime.
- PLAN schema: add per-task `dependsOn` (ids whose output the task needs).
- planBrief: require complete `dependsOn`; `layers` MUST be a topological order
(a task strictly after everything it depends on); each `validate` slice-scoped
(not a whole-suite e2e); merge two pieces that can't validate independently.
- validatePlan: ABORT a plan whose layers violate `dependsOn` (a task at/before a
dependency, or a dep not scheduled earlier / missing) — refuse to build on a
base where required upstream isn't integrated. Unit-tested against the 053 case.
- founderBrief (plan review): BLOCK incomplete deps, non-topological layers,
whole-suite validates, and mutually-blocking split tasks.
- loadPlanBrief (resume): preserve `dependsOn`.
SKILL.md guardrail + build-loop.md "Follow the DAG" section + README updated.
Template syntax-checked; topological mis-order detection unit-tested.
For a run launched before v1.5.0 (no status file, incl. an in-flight one),
`wf-status.mjs --write` now reconstructs a complete .ulpi/workflows/<run>.json
from the journal: plan name/path, working branch (from the preflight result),
the full task list with title/agent/branch + per-task status, layer count, live
counts + merge conflicts, inferred phases, and a planPath-based resume command.
The journal holds no launch args (gate config / hardRules / validate / prompt are
script inputs, not agent results), so `--args '<json>'` folds them in to complete
the resume recipe. Marked partial:true. Documented in references/status-tracking.md
(new "Backfill" section) + SKILL.md. Verified against a live 75-task run.
Two fixes to the delivery Workflow (forward-only; does not affect in-flight runs,
which execute from their own persisted script snapshot).
1. Slice-scoped per-task review (stops false BLOCK storms).
The build lands one DAG slice at a time, so per-task reviews ran against a
half-migrated tree and BLOCKed tasks for whole-codebase end-state gaps a LATER
task owns (e.g. "legacy paste path still exists" vs a task with no scope to remove
it) — burning all 3 fix rounds, then recording a false "blocked" (observed 72/81
blocked, ~184 BLOCKs dominated by one end-state finding).
- reviewerBrief now judges only the task's own writeScope + diff against ITS
acceptance criteria, and is given the rest of the plan so an unmet end-state
invariant a later task owns becomes an OBSERVATION attributed to that task,
never a BLOCK on the current slice.
- The fix loop acts only on in-scope blockers (fileInScope matcher); out-of-scope
findings are counted (crossTaskDeferred) and deferred, not retried.
- Impl review (step 12) is now the explicit end-state gate over the integrated tree.
2. Live status tracking.
- Durable <root>/.ulpi/workflows/<id>.json: skill creates it pre-launch (the
Workflow sandbox has no FS), stamps runId + resume command, and the Workflow
updates it at every phase + DAG layer via cheap haiku status-writer agents
(all writes sequential → race-free; non-fatal, never blocks the build).
- helpers/wf-status.mjs: journal reader reconstructing per-task status from the
run's journal.jsonl (zero template changes, works mid-flight); cwd-scoped,
--list/--json/--write, reads the durable file for phase/verdict. Ported and
generalized from a tool first built in the ulpi-v6 repo.
- Three verbs (status/stop/resume) documented; new references/status-tracking.md.
build-loop.md, playbook-state.md, SKILL.md (guardrails + EXTREMELY-IMPORTANT rules)
and README updated. Template + helper syntax-checked; fileInScope unit-tested 8/8.
- kiro-review SKILL.md: list valid kiro model names inline (auto, claude-opus-4.8/4.7/4.6,
claude-sonnet-4.6/4.5/4, claude-haiku-4.5) so it's self-sufficient; note opus/sonnet aliases invalid.
- ship-playbook: new KIRO_MODEL const (default claude-opus-4.8 = latest Opus, override via CFG.kiroModel)
injected into all three kiro briefs (build via hand-over-to-kiro, per-task review + routed review via
kiro-review) → '--model claude-opus-4.8'. Documented kiroModel in the args contract.
Helper gains --agent <name> (run kiro as a custom .kiro/agents/<name>.json agent) and
--model <name> (override the model). Documented the real kiro model names from
'kiro-cli chat --list-models' in references/kiro-cli.md → Models (auto, claude-opus-4.6/4.7/4.8,
claude-sonnet-4/4.5/4.6, claude-haiku-4.5, deepseek-3.2, minimax, glm-5, qwen3-coder-next).
Verified live (kiro-cli 2.6.0):
- A kiro-NATIVE agent (valid model + fs_read/fs_write/execute_bash tools) implements end-to-end.
- The ulpi .kiro/agents/*.json do NOT work as-is: model 'opus' is invalid (hard error) and the
Claude-style tool names (Read/Write/Skill) leave the agent with no working tools (writes nothing).
- --model auto rescues an agent that pins an invalid model.
So running specific agents works once the agent JSON is kiro-native (valid model + native tools) —
that generation is the AGENTS/ulpi ecosystem's job, not this repo. --skill injection remains the
zero-agent path that already works. Helpers byte-identical; bash 3.2 + set -u safe.
Doing skill-referencing in the helper, not as a prompt instruction the agent must
remember (same lesson as moving the prompt mechanics into the helper).
run-kiro.sh: new repeatable --skill <name> flag. Resolves .kiro/skills/<name>/SKILL.md
(or ~/.kiro/skills/<name>/), prepends it to kiro's STDIN under a <skill> tag with a
'read references/ on demand' note, and warns+skips an uninstalled name. Kiro has no
Skill tool, so this deterministic inject is how kiro follows a skill in a one-shot
--no-interactive run. bash 3.2/set -u safe (no arrays).
SKILL.md (both): Step 2.5 now just names the skills → pass --skill; removed the manual
<skills> prompt blocks. ship-playbook kiro brief passes --skill for the task's stackSkill.
Tested live end-to-end (kiro-cli 2.6.0): implement mode + --skill made kiro create a
file whose first line obeyed the injected skill's mandatory rule — proving the skill
drives behavior, not just that bytes arrived.
Kiro has a native skills feature (default agent auto-discovers .kiro/skills/ and
~/.kiro/skills/) but NO Skill tool and no --trust-tools token for it; skill bodies
load on demand, so a one-shot --no-interactive run can't rely on auto-activation
(confirmed against kiro.dev + AWS Amazon Q docs). The handoff never told kiro to
use the relevant skill, so kiro built without the stack conventions.
Fix (deterministic, no new tool/trust): the prompt-builder INLINES the relevant
.kiro/skills/<name>/SKILL.md into kiro's prompt under a <skill> tag and tells kiro
to follow it + fs_read its references/ on demand.
- hand-over-to-kiro: new Step 2.5 (reference relevant skills) + <skills> block in
both prompt templates + a Skills section in references/kiro-cli.md documenting
kiro's mechanism (auto-discovery, resources file://skill://, no Skill tool).
- kiro-review: inline a stack/convention skill so kiro reviews against it.
- ship-playbook kiro build brief: tell the handoff to inline the task's stackSkill.
Documents the native alternative too (--agent with resources:[file://...] preload).
The kiro skills hand-rolled a fragile inline launch (BSD mktemp choking on the
.txt suffix, stale files, kiro running on an empty prompt) and used
--trust-all-tools / -a even for read-only reviews (which the auto-mode classifier
blocks on non-mutating tasks). Modeled the fix on the codex companion: encapsulate
the mechanics in a helper, feed the prompt over stdin (not argv/shell).
helpers/run-kiro.sh (new, in both skills): locates kiro-cli, REFUSES to launch on
an empty prompt, scopes trust by mode (review=fs_read,execute_bash;
implement=fs_read,fs_write,execute_bash; autonomous=--trust-all-tools opt-in),
captures a git baseline, and feeds the prompt via STDIN (verified kiro-cli 2.6.0
reads stdin) — no mktemp, no heredoc, no argv, no shell parsing of the prompt.
SKILL.md (both): agent writes the prompt with the Write tool, then calls the
helper; trust scoped by task; never default to --trust-all-tools. ship-playbook
kiro build brief switched to implement mode (scoped write trust), not all-tools.
Tested live (kiro-cli 2.6.0): both helpers run review mode end-to-end, empty-prompt
guard returns exit 65.
Accept a pre-built, pre-reviewed plan and start at the build phase, skipping
plan-writing and plan-review. The skill passes either the parsed plan object
(args.plan) or a path (args.planPath); the Workflow sandbox can't read files, so
a path is loaded + validated + normalized by a load-plan agent. New validatePlan()
guards the DAG shape (tasks need id/agent/writeScope/validate; layers must
reference real ids) for supplied AND freshly-written plans, aborting cleanly if
malformed. PROMPT is no longer required when resuming (the plan is the spec).
Returns planSupplied so Phase 3 reports that plan-review was skipped by design.
SKILL.md: RESUME intake drops gate Q1 (plan writer) + Q2 (plan reviewer), asks the
other five, validates .ulpi/plans/<name>.json, passes planPath. README: note the
resume path. Distinct from the runtime's resumeFromRunId (mid-run crash recovery).
A rate-limit storm previously surfaced as a wall of false 'blocked: engineer
validate failed' tasks: agent() returns null when a subagent dies on a terminal
API error after the runtime's own retries (the 0-token door-rejections), or
throws on a transient fault (cut off mid-flight). Those are not real build/review
failures.
Added withRetry(fn) + sleep + isRateLimit + RETRY_DELAYS ([3s,10s,30s], up to 4
attempts). Retries on null return or a rate-limit-shaped throw (429/overloaded/
529/quota); genuine errors (e.g. agent-not-found) still surface immediately.
Wired into makeGate (covers the parallel engineers/reviewers/verify lenses) and
via a rAgent() wrapper on the 9 non-gated sequential calls (plan, preflight,
integrate/re-integrate, plan-fix, cleanup, routed reviews).
Sandbox-safe: no Date.now/Math.random; sleep is setTimeout-guarded and degrades
to immediate retry. Resume-safe: cached results return non-null on attempt 1.
Worst case unchanged (still null after 4 attempts). Concurrency caps and control
flow untouched.
- Write all artifacts under .ulpi/design/ (matching .ulpi/plans, .ulpi/issues):
DESIGN.md (locked, project-wide) + <feature>.md (flows, components, handoff).
- New design-system-routing.md: brief->system map (Radix/shadcn, Material 3,
Carbon, Fluent, Polaris, GOV.UK, HIG) + honesty rule; design_system field
locked in DESIGN.md. A design decision delegated to the builder, not code.
- Step 7 is now an explicit build handoff: target agent + chosen system +
acceptance criteria ('implement exactly this; do not redesign'). Reinforced
the boundary: building is delegated to an engineering agent; we own the spec.
- Dropped the optional mock-image line to stay a design-spec skill, not a
generator. Text/ASCII wireframes in the flow/component templates remain.
- Add sections + table rows for browse-qa, review-crate, bugfix-crate,
create-tests-extract, normalize-agent-for-claude, normalize-skill-for-claude,
nodejs, nestjs, docker (53 skills now documented, was 44).
- Rewrite ship-playbook to the current one-pass / seven-gate / no-autonomous-loop
reality (was described as a looping 2-question workflow).
- Rewrite frontend-design-ui-ux to v3.0.0: locked design language, anti-slop,
browse inspiration intake, scored pre-flight, a11y rigor, design-not-code.
- Note the new Available Skills & MCP feature in map-project and map-project-monorepo.
The build and verify phases fanned out (engineers per DAG layer, reviewers per
layer, and dedupVerify's one-verifier-per-finding x up to 2 lenses) bounded only
by the runtime's min(16, cpu-2) cap, enough agents in flight to trip Claude API
rate limits (429s). Added two independent concurrency gates: buildGate
(MAX_BUILD_PARALLEL=4) for the heavy isolation worktree engineers, agentGate
(MAX_PARALLEL=6) for the light read-only reviewers/verifiers. Gated at the leaf
spawns (buildSpawn, taskReviewSpawn, verifyFinding lenses) so the cap holds no
matter how parallel nests; serial fix loop and single agents untouched. Total
agent count unchanged, only concurrency is bounded. Constants tunable (not 1-2).
ship-playbook 1.1.0.
The audit fanned out finders, then one verifier per finding x up to 2 lenses,
plus a critic round, bounded only by the runtime's min(16, cpu-2) cap — enough
agents in flight at once to trip Claude API rate limits (429s). Added a global
concurrency gate (MAX_PARALLEL, default 6) and a gAgent() wrapper; routed every
spawn point (gates, finders, dedup, verify lenses, critic, follow-up finders)
through it. Control flow and parallel() barriers unchanged; total agent count
unchanged (~40-80) — only how many run at once is bounded, so they proceed in
waves. MAX_PARALLEL documented as a tunable (not 1-2). go-live-audit 1.1.0.
Both map skills now discover project-committed skills (.claude/skills/) and
enabled MCP servers (.mcp.json / .claude/settings*.json) and write an inline
'Available Skills & MCP — prefer these' section into CLAUDE.md (root CLAUDE.md
for the monorepo), plus a one-line pointer at the top of the file. Kept inline,
not in a lazy reference, because only always-loaded text changes behavior, so a
fresh agent stops forgetting the repo's own capabilities. New skills-and-mcp.md
reference carries discovery sources, redaction rules (never copy MCP secrets),
and the output format; SKILL.md steps, when-to-load, output-contract sections,
and verification gates updated. map-project 2.1.0, map-project-monorepo 3.1.0.
reviewerBrief told the read-only reviewer to confirm the per-task validate
'actually passes', so it re-ran the test command (vitest) in a read-only
sandbox; the runner's temp-dir mkdir EPERMs, surfacing a false 'tests not
green' CONCERN that re-triggered the fix loop even though integrate already
ran and passed that validate in a writable worktree. Now the reviewer reviews
statically and treats any check it cannot run in the sandbox as an OBSERVATION
(never BLOCK/CONCERN), so the loop converges once the real BLOCK is fixed.
Same guard added to implBrief and auditBrief.
Root cause of "ship-playbook can't run these": all three set disable-model-invocation: true, which blocks
the Skill tool entirely — so neither the ship-playbook orchestrator (Phase 3 map refresh) nor its
workflow subagents (kiro review role) could invoke them, even though they were installed and loaded as
user slash commands. A restart never fixed it; the flag is intentional, not a load-timing issue.
The flag was too blunt: the real intent (per the bodies) is "don't run PROACTIVELY" because they mutate
durable memory (map-project*) or run an external CLI (kiro-review) — but they SHOULD be composable by an
explicit user-invoked workflow like /ship-playbook. Fix: remove disable-model-invocation (keep
user-invocable: true so the slash commands still work), and reword the EXTREMELY-IMPORTANT rule +
Guardrail from "explicit-user-only" to "no proactive invocation, but composable by an explicit
user-invoked workflow". Not changed: claude-review / codex-review stay user-only — ship-playbook does
not compose them (native review is an inline agent, codex review is the codex:codex-rescue plugin).
A live run showed reviewer agents as "nextjs-reviewer"/"go-reviewer" — wrong. The reviewer is the FULL
engineer name + "-reviewer": nextjs-senior-engineer + nextjs-senior-engineer-reviewer, go-senior-engineer
+ go-senior-engineer-reviewer, etc. (matches the registry and the workflow's `reviewer = agent + '-reviewer'`).
The Step 1 examples were ambiguous ("nextjs-senior-engineer(+-reviewer)"), so the instance abbreviated
wrong. Now the skill spells out the full pair names and the install commands
(@nextjs-senior-engineer + @nextjs-senior-engineer-reviewer), and explicitly says never to shorten to
@nextjs-reviewer.
Follow-up to the loaded-list fix: a skill installed to .claude/skills/ is only loaded into the
available-skills list at Claude STARTUP, so "installed on disk" != "usable this session". The previous
"use the loaded list, not disk" rule was right about usability but mislabeled an installed-but-not-yet-
restarted skill as plain "missing" — confusing when the skill IS available on disk.
Now three states:
- ready: in the loaded available-skills list / subagent_type options → usable now, not listed.
- installed, needs restart: not loaded, but present on disk via a SYMLINK-AWARE check
(test -e / ls -laL .claude/skills/<name>, NOT find -type d) → shown with "Restart Claude to load",
no install command.
- missing: not loaded and not on disk → shown with the full install command.
The two tables now list not-ready items with their state+action; the "present/ready" section is
explicitly forbidden (only show what needs action). kiro-cli binary detection is separate (name +
~/.local/bin PATH fallback).