Loop Build — phase 2 of loop-run
Input: an ISSUE-ID (e.g. <TEAM>-<N>). State dir: $LOOP_STATE_DIR/<ISSUE-ID>/ (default ~/.claude/loop-state/<ISSUE-ID>/). Requires spec.md there — if missing, stop and tell the operator to run loop-spec first.
Repo: a lago-front checkout. Two layouts, resolved by loop-run and defined in its ## Layout section — worktree (the default: front/ in the lago monorepo, worktrees in front-worktrees/ beside it) and in-place (the current checkout is the worktree; automatic inside a Conductor workspace, forced with --in-place). Invoked directly without loop-run, resolve it the same way: $CONDUCTOR_WORKSPACE_PATH set or --in-place passed → in-place, else worktree.
Nothing is fetched. spec.md carries the ticket and its sources; the coding styleguide is .agents/docs/frontend-coding-styleguide.md. No Linear, no Notion in this phase.
Modes
- Fresh build: no
review.mdin the state dir, or it says PASS. - Fix mode:
review.md(oradversarial.md) has verdict FAIL, orci-failure.mdwas just written by loop-run — fix ONLY the numbered issues / CI failures listed there, nothing else. Reuse the existing worktree fromstate.md.
Steps (fresh build)
Preflight — the
worktreelayout creates its workspace,in-placevalidates the one it was handed:worktreelayout (all must hold, else STOP and ask the operator):- Main docker stack running:
docker ps --format '{{.Names}}' | grep lago_front_dev. - Local
maininfront/up to date:git -C front pull --ff-only origin main. If it fails (dirty checkout, diverged), STOP — never stash, reset, or force. - If a
front-worktrees/<ISSUE-ID>-*dir already exists from an aborted run, STOP and ask — never delete or force.
in-placelayout:FRONT="$(git rev-parse --show-toplevel)" [ -x "$FRONT/scripts/iter-budget.sh" ] || { echo "not a lago-front checkout"; exit 1; } BRANCH="$(git rev-parse --abbrev-ref HEAD)" git fetch origin main- Branch guard: STOP if
BRANCHismain(or$CONDUCTOR_DEFAULT_BRANCH), or isHEAD(detached). The loop only ever works on a feature branch. git fetch origin mainis required: the review phase diffs againstorigin/main. Fetch only — NEVER pull, rebase, merge, stash or reset (the checkout is the operator's).- State conflict: if
state.mdalready exists for this ISSUE-ID and itsworktree:is a different path, STOP — that state belongs to another checkout. - Docker stack (
docker ps --format '{{.Names}}' | grep lago_front_dev): missing → warn only, never block. Gates run on the host.
- Main docker stack running:
Create the worktree —
worktreelayout ONLY; inin-placeskip this step entirely (nothing is created,lago-worktreeis never called). Use the repo's own tool (handles branch, .env copy, pnpm install, port slot, dedicated docker containers, isolated API worktree):lago-worktree create <BRANCH> --from-front=main --from-api=main(
lago-worktree=front/scripts/lago-worktree.sh; if the alias is unavailable, call the script directly.)Branch naming —
<BRANCH>=<ISSUE-ID>-<topic-slug>: the Linear issue ID first, UPPERCASE, then a short kebab-case slug of the ticket's main topic (3-6 words), e.g.<TEAM>-<N>-swap-customer-overview-connection. No Linear ticket (edge case, e.g. tooling change requested directly) → just the kebab-case topic slug:clean-vite-cache-on-worktree-start. Worktree dir name = branch name. The session stays in the lago root — operate on the worktree viagit -C/cdin subshells.In
in-placethe branch is whatever the checkout already carried and is never renamed (git branch -mis forbidden: Conductor persists the branch name in its own database, so a git-side rename desyncs its diff view and its archive-time branch deletion).Record state: write
state.mdin the state dir (keyed on the bare ISSUE-ID). Key names are load-bearing —loop-review,loop-reviseandscripts/loop-restart.shread them:layout: <worktree | in-place> worktree: <absolute path — front-worktrees/<BRANCH> in worktree layout, the checkout itself in in-place> branch: <BRANCH> port: <front port printed by lago-worktree, or $CONDUCTOR_PORT in in-place> workspace: <$CONDUCTOR_WORKSPACE_NAME — in-place under Conductor only> container: <lago_front_wt_<SAN(branch)> | lago_front_ct_<SAN(workspace)> — omit when there is none>SAN=tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g'on the branch (worktree layout) or on the workspace name (in-place).Plan before code — write
plan.mdin the state dir. It is the declarationscripts/loop-plan-check.shand the reviewers compare the diff against; a new file or export that appears in the diff without appearing here triggers the adversarial pass:# <ISSUE-ID> plan ## Files to change <path — one line: what changes> ## New files <path — why no existing file could host it, naming the sibling checked; or "none"> ## New exports <symbol (path) — who consumes it, why inlining at the call sites is worse; or "none"> ## Estimated size <N lines> ## Comments kept <one line per comment the diff keeps: `- <path> — constraint|why-not|trap — "<first words>"`; or "none"> ## Deviations <appended during implementation: what changed from the plan above and the one-line reason>Start from spec.md
## Files to touchand## Premises. The default for## New files,## New exportsand## Comments keptis "none": write the diff in your head without the abstraction first, and add it only when the inline version is larger. Read.agents/docs/frontend-coding-styleguide.mdnow, before the first edit.Implement per spec.md and plan.md, inside the
worktree:path from state.md only (inin-placethat is the cwd). The checks below are capped byscripts/skill-budget.sh.- The plan is the scope. Reality diverges from spec.md or plan.md → append the divergence and its reason to plan.md
## Deviationsand continue only if minor; otherwise stop and report. A premise spec.md marksunverifiedis not built around: build what the code shows, and say so in the report. - Existing mechanism first. Before writing a component, hook, util or handler, search
lago-design-system, the shared modules and the global layers (Apollo error link, toast, router wrappers) for one that already does the job — reuse or extend, never duplicate. When the spec says to mirror a sibling, diff YOUR dependency list against ITS: a dependency the sibling deliberately avoided (an aggregate hook, a wider query) needs a reason in the report. - Translations (
translations/base.json): search for an existing label with the same meaning before adding a key; new keys only when nothing fits, following the naming pattern; remove a key when the change removes its last usage. - Comments are declared or deleted.
scripts/diff-hygiene.shflags every comment the diff adds unless plan.md## Comments keptlists it with its category —constraint(an external rule the reader cannot see, identifier named),why-not(the obvious alternative is wrong),trap(an edit here breaks something elsewhere) — and refuses outright a comment inside atype/interfacebody, above an import, one that repeats the export's name, or one over 2 lines. Nothing else is a category: "explains the prop", "describes the step", "summarises the function" are deletions. Expectnoneon a typical diff. - Never
as unknown asyour way past a type. A shared component's prop type: narrow toPick<...>of the fields it reads. Two enums with identical members: an exhaustiveRecord<Source, Target>lookup, the only form that breaks the build when one enum grows. - Copied state goes stale. A config snapshot (
MainHeader.Configure), a draweropen({ children }), a child seedinguseStatefrom a prop: each copies a mutable value once. Either encode every field it reads in a key (remount / snapshot key) or pass a getter / read the prop. A parent that resets a value the child also stores is a silent wrong-save. - A redirect in a
useEffectdoes not stop its own render: pair every guard effect with an earlyreturn nullon the same condition (after all hooks), and assertcontainer.firstChildis null.
GraphQL documents changed →
pnpm codegen, and when the worktree's API runs a feature branch, keep only the hunks insrc/generated/graphql.tsxthat belong to your own operations (CI regenerates from lago-apimain).- The plan is the scope. Reality diverges from spec.md or plan.md → append the divergence and its reason to plan.md
Tests — ALWAYS: after the implementation is complete, invoke the
make-testsskill on the changes. NEVER hand-write tests.Gates (run in the worktree, all must pass):
pnpm lint(usepnpm lint:fixfirst if there are formatting issues),pnpm types,pnpm translations:inspect,pnpm translations:ensure-consistency.<front>/scripts/diff-hygiene.sh origin/main <worktree> <state dir>/plan.md— every added comment is declared with a category, none in a refused position. Flagged → delete it (the default) or, for a real constraint/why-not/trap, add its line to## Comments kept.<front>/scripts/loop-plan-check.sh <worktree> <state dir>/plan.md— every new file / export / re-export is declared. Exit 1 → either delete the abstraction or declare it in plan.md with its reason; never leave it undeclared. Exit 3 (all declared) passes the gate: it is loop-run's signal to run the adversarial pass on what was declared.- Scoped jest on the paths make-tests produced/touched. NEVER run the full suite (
pnpm testwith no path is FORBIDDEN). - A jest failure that does not reproduce in isolation or under
--runInBandis load/cold-cache flake, not a regression: rerun before diagnosing or consuming a cycle.
Report: diff stat, plan.md deviations, gates output summary. Do NOT commit — shipping happens in loop-run after review PASS.
Steps (fix mode)
Read the numbered issues from
review.md/adversarial.md(or the failure report inci-failure.md).ci-failure.mdis already distilled by loop-run — never open theci-raw-<N>.logit references; if it looks insufficient, grep that raw file for the one specific symbol you need, never read it whole.Escalating retry — attempt N>1 must not be a blind rerun of attempt N-1:
- Read the full history too:
review-history.md/ci-failure-history.mdin the state dir. - Before coding, state in your working notes, per issue: what the previous attempt did and what THIS attempt does differently.
- Same issue failed twice → the previous strategy is wrong: re-read spec.md acceptance criteria from scratch, broaden the investigation (callers, related components, existing tests), question the diagnosis itself. Consume the retry, but on a different path.
- Oscillation check: a fix must not revert a change made by a PREVIOUS iteration. Detected → declare it, do NOT apply either oscillating fix again: find the third option one level up from where both were applied, and note it for loop-run's flywheel.
- Never STOP early for a repeated failure — the 3-attempt budget belongs to loop-run and is enforced by
<checkout>/scripts/iter-budget.sh.
- Read the full history too:
Fix only those issues, in the
worktree:path from state.md. A fix does not earn a comment: if the reviewer misread the code, rename or split it; the reasoning goes in the report and the commit body. An[adversarial]issue is fixed by inlining or deleting, never by justifying the abstraction in plan.md after the fact.Operator input that changes behaviour (a design, a copy change, a new rule) is a spec amendment, not a patch: write the new acceptance criteria into spec.md FIRST and enumerate the state transitions they imply (mount / select / change / revert / user-edited / locked), then code.
Re-run the gates (step 7 above). If the fix touched testable logic, re-invoke
make-testsfor the affected paths.Report what changed per issue number, including the "what's different from the previous attempt" line for each.
Hard rules
- All edits in the
worktree:path from state.md. In theworktreelayout that means never the mainfront/checkout; inin-placenever$CONDUCTOR_ROOT_PATHand never another workspace. in-placecreates and destroys nothing: nolago-worktree, no branch rename, no Conductor workspace created, archived or renamed.- NEVER
git stashthere: the review phase leavesgit add -Nentries and the pop conflicts. Compare against the base withgit show <base>:<path>, or in a throwawaygit worktree. - No commit, no push, no PR in this phase. Never fetch Linear or Notion.
- Never run the full jest suite. Tests only via the make-tests skill.
- Two communication registers: messages to humans (chat report, notifications) = short, direct, plain language, no deep-tech jargon. Internal state files (spec.md, plan.md, review.md, histories, working notes) = written for the AI of a later iteration: dense, precise, full paths/symbols/error strings — optimize for machine effectiveness, not human readability.