agentleFS
AznatkoinyAznatkoiny/plugin-forge/skills/build-loop/SKILL.md

build-loop

Skill1 starsChanged 3 months agoMITView on GitHub

Launch the isolated build-until-green loop for a forge project. Run when .forge/state.json shows phase=armed, after /plugin-forge:arm-evals. Walks the preconditions checklist (trust dialog, hooks enabled, MCP OAuth preflight for live servers, worktree base ref, permissions.allow for a long unattended run), then starts bin/forge-build in background Bash: a git worktree seeded with the frozen suite plus a headless /goal session that iterates until forge-eval prints the FORGE_EVAL scoreboard line with RESULT=PASS without touching evals/. Tails progress, applies the StopFailure resume-not-respawn recipe on rate_limit/overloaded, and on PASS hands off to /plugin-forge:verify. Use forge-build --no-goal when hooks are disabled.

3 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

6 rules. 0 are common, 1 is distinctive, and 5 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If the first line of this skill rendered as literal backtick syntax instead of JSON or NO_STATE (org policy disableSkillShellExecution …

    line 12
  2. Distinctive:

    Read tool before doing anything else.

    line 14
  3. Very specific:

    Read plugin_name and plugin_dir from .forge/state.json for everything below.

    line 32
  4. Very specific:

    Run forge-build via the Bash tool with run_in_background: true — never in the foreground.

    line 100
  5. Very specific:

    Check the background task's output periodically.

    line 109
  6. Very specific:

    When the run completes, cat the final FORGE_EVAL: line from the harness output into the transcript so the tool-less goal evaluator can see …

    line 153

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

!cat .forge/state.json 2>/dev/null || echo NO_STATE

build-loop — launch the goal loop against the frozen suite

If the first line of this skill rendered as literal backtick syntax instead of JSON or NO_STATE (org policy disableSkillShellExecution blocks dynamic injection), read .forge/state.json with the Read tool before doing anything else.

State guard

Route on the injected state above. Refuse out-of-order execution and give the exact remediation command.

Injected stateAction
NO_STATERefuse: no forge project here. Remediation: run /plugin-forge:forge
phase: interviewRefuse: pipeline not started. Remediation: run /plugin-forge:forge
phase: contractRefuse: no approved contract. Remediation: run /plugin-forge:write-contract
phase: evalsRefuse: the suite exists but is not armed — nothing binds the builder yet. Remediation: run /plugin-forge:arm-evals (or /plugin-forge:build-evals first if evals/ is missing)
phase: armedProceed — this is the launch phase
phase: buildingRefuse to launch a second loop. Check the background forge-build task and report its latest FORGE_EVAL line; if the previous run crashed and state is stuck, run forge-eval doctor, then /plugin-forge:arm-evals if the freeze needs repair
phase: smoke or phase: verifyRefuse: the build already went green. Remediation: run /plugin-forge:verify
phase: shipRefuse: this cycle is done. Remediation: run /plugin-forge:ship, or start a new iteration via /plugin-forge:arm-evals after suite changes

Read plugin_name and plugin_dir from .forge/state.json for everything below.

What forge-build does (so the launch is informed, not blind)

bin/forge-build verifies phase == armed and that the evals/ tree hash still matches .forge/freeze.json, then:

  1. Creates an isolated git worktree at .claude/worktrees/forge-build-<ts>.
  2. Seeds the worktree with design/CONTRACT.md, the frozen evals/ tree, templates, and — critically — .forge/state.json with phase=building plus .forge/freeze.json. The seeding is what arms the guard hooks inside the child session; without state + freeze in the worktree the "unbypassable rail" silently never engages. evals-holdout/ is deliberately NOT seeded — the builder must never see the holdout tranche. Within eval trials, the harness additionally stages each task's reference/ answers outside the trial workspace.
  3. Launches a headless builder: claude -p "/goal ..." --output-format stream-json --verbose with a scoped --allowedTools list (Read, Edit, Write, Glob, Grep, Bash(forge-eval *), Bash(claude plugin validate *), and similar — never a blanket grant).
  4. On success cleans up its worktree; on failure leaves it in place, flagged, for triage. (--keep-worktree preserves it either way.)

While phase=building, the PreToolUse guard (guard-evals.sh) exit-2 blocks any Edit/Write/Bash mutation under evals/, and the Stop-hook sweep (sweep-evals.sh) recomputes the tree sha256 against the freeze every turn — these hold even under --dangerously-skip-permissions. The same guards apply in the parent project: do not edit evals/ from this session either.

Preconditions checklist (walk it, do not skip)

  1. Trust dialog accepted. The /goal evaluator rides the hooks system; the project must have passed the trust dialog at least once (open claude interactively in the project if unsure).
  2. Hooks enabled. disableAllHooks at any settings level, or allowManagedHooksOnly in managed settings, makes /goal unavailable — fall back to forge-build --no-goal. Warn loudly in that case: with hooks disabled the guard hooks and the Stop-hook sweep are dead too, so the freeze is only checked after the fact — instruct verify to recompute the tree hash against .forge/freeze.json before trusting any green result, and get explicit user acknowledgment before an unattended run without rails.
  3. MCP OAuth preflight. For every LIVE server in CONTRACT.md's live-vs-mock table: OAuth cannot happen inside claude -p, so authenticate each live server in an interactive session first; tools marked requiresUserInteraction are force-denied headlessly (plan mocks or drop those paths); elicitation-using servers need the generated Elicitation auto-answer hook plus elicitation_answers fixtures in the affected tasks. Mocked servers need nothing.
  4. Worktree base ref decision. worktree.baseRef is "fresh" (branch from the remote default branch) or "head" (branch from the current local HEAD). Choose "head" when the target project's fixtures or supporting code are uncommitted/unpushed — "fresh" would build against a tree that lacks them (forge-build's seeding covers only contract, evals, templates, and forge state). Choose "fresh" for a clean default-branch baseline. baseRef cannot name a branch; for a pinned branch, create the worktree manually with git worktree add and point forge-build at it.
  5. permissions.allow for a long unattended run. Skill allowed-tools grants are per-turn, so a multi-hour run will stall on prompts unless the user pre-approves. Suggest (never silently write) adding to the target project's .claude/settings.local.json:

    {"permissions": {"allow": [
      "Bash(forge-eval *)",
      "Bash(forge-build *)",
      "Bash(git worktree *)",
      "Bash(claude plugin validate *)"
    ]}}

    Worktrees share approvals saved to the main checkout's settings.local.json, so approving once covers the build worktree. The child builder's tools come from forge-build's --allowedTools; this list keeps the PARENT (this) session from stalling while tailing and re-running gates.

Also run git worktree list and forge-eval doctor if a previous run crashed — leaked forge-build-* worktrees and stuck phase=building state must be cleared before relaunching.

Launch

Run forge-build via the Bash tool with run_in_background: true — never in the foreground; a real build loop runs 30+ minutes to hours and a foreground call would die at the Bash tool timeout:

forge-build --watch

While it runs:

  • Check the background task's output periodically. Surface every FORGE_EVAL: line to the user as it appears (each one is an iteration scoreboard) and report cumulative cost_usd.
  • Guard blocks (BLOCKED by plugin-forge: the eval suite is FROZEN...) in the stream are the rail working, not an error: the builder tried to "fix" a failing grader. It is instructed to route such desires into .forge/eval-change-requests.md instead; if requests accumulate, tell the user a human review + /plugin-forge:arm-evals re-arm may be warranted.

The goal condition (what "done" means, mechanically)

forge-build issues a goal of this shape:

/goal Build the plugin at <plugin_dir> so that `forge-eval run --suite capability --plugin-dir <plugin_dir>`
prints a final FORGE_EVAL line with RESULT=PASS, and `claude plugin validate --strict <plugin_dir>` passes,
WITHOUT modifying anything under evals/ (frozen contract, sha256 in .forge/freeze.json).
Print the full FORGE_EVAL line each iteration. Stop after 40 turns if not achieved.

Three design facts make this work:

  • The goal evaluator is TOOL-LESS. It judges only the conversation — it runs no commands and reads no files. The scoreboard line (shared contract, design doc 5.2)

    FORGE_EVAL: suite=<name> version=v<N> passed=<X>/<Y> pass^<k>=<0.00> cost_usd=<C> RESULT=PASS|FAIL

    printed by the harness as its final stdout line is therefore the only admissible proof: it must land in the builder's transcript. A builder that stops printing it stalls the loop — the evaluator can never judge the condition met.

  • The invariant clause ("WITHOUT modifying anything under evals/") is judged by the evaluator AND mechanically enforced by the guard hooks — belt and braces.
  • The turn bound ("stop after 40 turns") is judged from the conversation, so the builder must report progress against it each iteration.

Suite runs INSIDE the builder session: background Bash + progress file

A real suite (tasks x trials) runs far longer than any foreground Bash tool call allows — a foreground forge-eval run inside the builder session would time out mid-suite and the iteration would be lost. The builder session must instead:

  1. Launch forge-eval run --suite capability --plugin-dir <plugin_dir> with Bash run_in_background: true.
  2. Poll the harness's resumable progress file under the run directory (runs/<ts>/) between turns.
  3. When the run completes, cat the final FORGE_EVAL: line from the harness output into the transcript so the tool-less goal evaluator can see the proof.

forge-build injects these instructions into the builder prompt. When driving a builder session by hand (debugging, --no-goal development), follow the same recipe — it is the difference between an iteration that counts and one that dies at a tool timeout.

Retry budget

  • Soft budget — the turn-bound clause. "Stop after 40 turns if not achieved" is held by the evaluator from the conversation. Adjust per contract complexity via forge-build's options.
  • Hard backstop — the Stop block cap. /goal is a session-scoped prompt Stop hook, and Claude Code overrides any Stop hook after 8 consecutive blocks without progress. Every "not achieved yet" verdict is a block: a builder showing no measurable progress for 8 turns is cut off regardless of the 40-turn clause. In goal mode, forge-build exports CLAUDE_CODE_STOP_HOOK_BLOCK_CAP=25 into the child goal session's environment to raise that cap for long builds; a fresh FORGE_EVAL line with a changing passed=<X>/<Y> is the cleanest form of visible progress.
  • --max-iterations N bounds the --no-goal outer loop.

StopFailure resilience: snapshot + resume, never respawn

API failures do not fire Stop — they fire StopFailure (matcher values include rate_limit, overloaded, server_error, ...). A rate-limit spike can therefore kill the loop in a way the goal evaluator never sees. The recipe (shipped as ${CLAUDE_PLUGIN_ROOT}/templates/hooks/stopfailure-resume.sh):

  • A StopFailure hook matched on rate_limit|overloaded snapshots resume state — session_id, worktree path, latest FORGE_EVAL line — into .forge/ (see the template header for the exact path). StopFailure hook output and exit codes are ignored by Claude Code: the hook records; it cannot block or retry by itself.
  • Resume, do not respawn: claude -p --resume <session_id> from the project directory re-enters the build worktree with the active goal restored (turn counters reset). A fresh session instead loses the goal, the iteration context, and the guard-armed worktree association, and re-burns cost rediscovering all of it.
  • forge-build distinguishes StopFailure-terminated runs (resume candidates — the snapshot exists and the last FORGE_EVAL was progressing) from genuine non-convergence (triage candidates). Report which one happened; only resume the former.

Outcomes

  • RESULT=PASS in the final FORGE_EVAL line: forge-build cleans up its worktree. Report the final scoreboard line and total cost, then invoke /plugin-forge:verify — it runs the cheap smoke matrix first (phase smoke), then the expensive fresh-trial pass^k, holdout tranche, and trigger suite (phase verify). The build going green is necessary, not sufficient: the honest claim is "green + holdout + triage", never "correct".
  • Non-convergence (turn bound, block cap, or --max-iterations exhausted): the worktree is left flagged for inspection. Dispatch the triage agent over the failing runs/<ts>/ trials; it labels each failure agent-error | grader-error | task-ambiguity. Grader-error findings go through .forge/eval-change-requests.md and a human re-run of /plugin-forge:arm-evals (version bump + re-freeze) before relaunching /plugin-forge:build-loop; agent-errors usually mean more budget or a contract fix.
  • StopFailure-terminated: apply the resume recipe above.
  • Crashed/stuck (phase=building with no live process, leaked worktrees): forge-eval doctor.

Cost honesty: a 20–50-task suite at trials=3 plus a 40-turn builder can run tens of dollars per cycle. forge-eval --max-cost-usd hard-aborts runaway suite runs inside the loop; watch cost_usd in every FORGE_EVAL line and keep the user informed of the running total.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.