# Craft

> Use when the user says "craft this", "run a craft loop", "do this properly", "take this through clarify plan and verify", "run it through the gate", "/craft", or hands over a substantial change that has no domain workflow of its own and should be planned, approved and independently verified before it lands. NEGATIVE ROUTING: a code change or bug fix is /dev; a dataset, table, figure or number is /ds; long-form prose is /writing; a talk built from a research paper is /workshop; lecture notes or course slides are teaching:notes and teaching:slides; a skill, workflow or plugin in this repo is skill-creator, workflow-creator or plugin-creator. Each of those is this loop plus a domain gate, and craft is only the fallback when none of them fits.

- Skill: `edwinhu/craft` (Agent Skill, multi-file: 48 files)
- Install (CLI): `npx skillmds@latest add edwinhu/craft`
- Raw SKILL.md: https://api.skillmd.com/api/skills/edwinhu/craft/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: edwinhu (https://skillmd.com/u/edwinhu)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/edwinhu/craft

---


# craft — clarify → plan → goal → workflow → human review

A structured loop for tasks worth doing properly: clarify with the user, draft a plan they
edit and approve, self-set a goal, run `workflow.js` — dispatched through farm-out — to implement
and independently verify, then put the result in front of the human in tuicr. Human rejection
routes back to CLARIFY.

```
 ┌────────── human REJECT / criteria wrong ──────────────────────────────┐
 ▼                                                                       │
CLARIFY ─► PLAN ─► GOAL ─► workflow.js ──PASS──► HUMAN REVIEW (tuicr) ──┤
 (ask)    (draft,  (self-   IMPLEMENT, then       │ approved → done   │
           user     send)   VERIFY ∥ MECHANICAL   │ findings → fix ─► re-run
           edits)           ∥ third-party (opt-in,│            workflow.js (subset)
                            advisory) → JS gate   │
                              ▲    │
                              └─fix┘ FAIL — re-run selector is tasksThatFlagged
                                    + mechanicalThatFailed + lensesThatFlagged
```

**Plan review is computed and happens before dispatch, not inside it** — `plan-lint.ts` over the
built args and `plan-preflight.ts` executing their commands at baseline, enforced by
`craft-dispatch.sh` while the run is still armed. No agent reads the plan markdown for defects
(see *Plan review*).

Everything lives in this skill directory — `workflow.js`, `references/third-party.md`,
`scripts/craft-dispatch.sh` (Phase 3+4 in one call), `scripts/craft-pending.sh` (is a dispatch
owed?), `scripts/goal-self-send.sh`, `scripts/human-review-gate.sh`, `scripts/craft-result.sh`.
Nothing here depends on any plugin.

**The gate has a test suite; run it after touching `workflow.js`** — `node --check` proves only that
the file parses. `scripts/workflow-harness.mjs` executes the script for real against stubbed hooks,
and `scripts/workflow.test.ts` asserts the invariants that decide PASS/FAIL: fail-closed on every
dead agent, the three `redCommand` verdicts, `overallPass === false` implying a non-empty selector,
the `readOnly` n/a dimensions. `scripts/plan-review.test.ts` guards the computed plan-review rules
and the absence of the judged layer. Add both to `mechanicalChecks` on a run that edits the spine:

```bash
bun test ${CLAUDE_PLUGIN_ROOT}/skills/craft/scripts/   # absolute path: a bare relative path is
# read as a NAME FILTER and exits 1 having matched nothing — identical to a real failure
```

## State

Two locations, one owner each:

| Path | Holds | Owner |
|---|---|---|
| `<plansDirectory>/<slug>.md` | the approved plan — **the run's authority**, the file that gets hashed | plan mode (native); craft only reads and hashes it |
| `.craft/<run-id>/` | args, verdict JSON, and `plan-<hash12>.md` — the archived bytes each round ran under | craft |

`plansDirectory` decides where that plan lives, and craft **honours whatever it is set to** —
`"./.claude/plans"` and `"./.planning"` (what the domain workflows use) are
equally valid. The value is resolved relative to the project root, so the plan is project-local and
craft hashes it in place — no copy. Unset at every tier, the default is `.claude/plans`. `run-id` is
a short date-slug like `0806-fix-auth`. Both `.craft/` and the plans directory want to be
gitignored — add them before the
first run if the repo would otherwise track them. Clean up `.craft/<run-id>/` when the run
completes, unless the user wants provenance kept; leave the plan file alone either way, it's plan
mode's. **The plan file is not durable and craft does not own it** — plan mode memoizes one slug per
session, so re-entering plan mode overwrites the plan in place, and the directory is gitignored. So
dispatch archives the bytes it hashed to `.craft/<run-id>/plan-<hash12>.md` (content-addressed: an
amended round adds one, never overwrites). That archive is the only copy of what a run was approved
with — if the plan matters beyond the run, `git add -f` it before cleaning the run dir. There is no `goal.md`: the plan holds the success criteria, and the goal is a
session-level condition (Phase 3), not a file.

## Phase 1 — CLARIFY

**Before any task reconnaissance**, AskUserQuestion on the axes that shape the plan (skip axes
the request already answers; batch up to 4 per call):

1. Desired outcome — what does done look like?
2. Exclusions — what must NOT change?
3. Constraints — style, deps, compatibility.
4. Observable success criteria — what command/check proves it? *A command with a meaningful
   exit code becomes a `mechanicalChecks` entry in the plan's Run sizing block.*
5. Review surface — working tree (default), commit range, or PR?
6. **Third-party review?** — none (default) / codex / gemini / both. *This is the only opt-in
   moment; a plan approved without the opt-in line never runs third-party.*
7. **Read-only runs only — use an agent team for discovery? (default yes.)** Ask this axis only
   when the run is an audit (`readOnly: true`); for a run that writes, the answer is always no and
   the axis is skipped. The honest tradeoff: a team of communicating auditors catches **cross-file**
   defects that isolated lenses structurally cannot see — each lens judges alone and no lens holds
   two files at once. The cost is that a team's findings are **correlated**, so refutation must stay
   **outside** the team. Answering no is a real option; it costs discovery breadth, not gate
   integrity. For where the team runs and how its findings reach the gate, see *Where the agent team
   lives*.

Gate: you can plan without guessing. If answers surface a trivial task, say so and exit the
loop — see red flags.

## Phase 2 — PLAN

EnterPlanMode. Explore, then draft a plan that MUST contain:

- **Task table**: `(id, name, work, writable paths, acceptance)` — acceptance is a checkable
  criterion per task, not a vibe. Add **`dependsOn`** to any row whose refs or inputs are files
  another row writes: it is the only thing that orders IMPLEMENT, and rows without it are
  implemented concurrently (so their writable paths must be disjoint).
- **Every claim is a `tasks[].acceptance` clause or a `mechanicalChecks` entry**, plus the review
  surface. A criterion belonging to no task belongs nowhere: it is a sentence nothing runs, and it is
  the largest defect surface a plan has. Whole-deliverable facts become a mechanicalCheck; per-task
  facts become that task's acceptance. Prose may explain WHY, never assert WHAT — the moment prose
  states a criterion there are two representations of one fact, and they drift within one amendment.
  `plan-lint`'s `prose-command` rule enforces the executable half: a command in prose that no
  acceptance, redCommand or mechanicalCheck runs is a MAJOR.
- **Run sizing** — the scrutiny the gate will apply (Phase 4 documents what each knob costs):

  ```
  ## Run sizing
  Review lenses:     criteria-vs-artifacts, scope-fidelity   (+ one line per added lens, with the risk it covers)
  Mechanical checks: <name> — `<exact command>`              (omit the section if none)
  Scored checks:     <key> — <what it scores>, ADVISORY: never gates  (opt-in, no default; omit if none)
  Test-first:        <task id> — `<redCommand>`               (one line per red-gated task; omit if none)
  Red dispositions:  <task id> — <why this task carries no red gate>  (one line per dispositioned task; omit if none)
  Third-party review: codex                                   (only if opted in at CLARIFY)
  ```

Every task fans out to 1 implementer + 1 verifier, plus 2 probes if it carries a `redCommand`; every
lens to 1 reviewer + up to `refutersPerLens` refuters; **every `priorFindings` entry costs one
refuter**; every `scoredChecks` item costs one agent, advisory or not. If that runs past ~50, the
plan is too coarse-grained for one gate: split it into sequenced craft runs.

**This is enforced, not advised.** `workflow.js` computes its own fan-out floor
(`2·tasks + 2·redGatedTasks + lenses + mechanicalChecks + scoredItems + priorFindings + thirdParty`)
at arg-validation and **throws before dispatching anything** if it exceeds `maxAgents` (default 50);
the error prints the per-dimension breakdown. Raising `maxAgents` is legitimate; raising it silently
at dispatch time is what the throw prevents, because sizing is the user's call at approval time.

**Sizing lives in the plan because it shapes the gate.** Choosing lenses or dropping a mechanical
check after approval would weaken the verdict without changing a byte the user signed off on — the
hash covers the `craft:dispatch` spec block, so anything that decides PASS/FAIL has to be inside it.

**An audit needs a plan too.** `readOnly` still requires `planPath` + `specHash`, and the plan it
hashes is a **charter**, not a work order: what is being audited, which lenses judge it, which
mechanical checks run, and the standing instruction that nothing may be written. The task table may
be empty. Do **not** shortcut by hashing the artifact under audit instead: the AUTHORITY block tells
every agent the hashed file is its *only authority*, so hashing the audited file would tell each lens
that the thing it is judging is the standard it judges against.

**Arm the run before calling ExitPlanMode.** The plan must carry a dispatch block — every
`workflow.js` arg except `planPath`/`specHash`, which `craft-dispatch.sh` injects because a block
cannot state its own hash:

```
<!-- craft:dispatch
{"runId": "0813-slug", "goalTurns": 12, "args": { … }}
-->
```

Writing it is what arms the run, and the plan is the only file plan mode may write — which is also
the only thing that survives approval. **Claude Code clears the context when a plan is approved near
the ceiling** and re-seeds a bare `Implement the following plan:` session with no craft in it, which
will otherwise implement in the main thread. While a plan is armed and no `.craft/*/args.json`
records its hash, `~/.claude/hooks/main-thread-guard.sh` denies Edit/Write/Agent in that project —
resolving the project from the nearest ancestor of `cwd`, so a `cd` cannot disarm it — and blocks the
turn from ending once; both name the dispatch command. It denies only what some task's
`writablePaths` covers (`craft-dispatch.sh --covers`, failing closed when the spec cannot decide):
a path no implementer may write is not the run's output, which is what leaves the **red suite
authorable** here and nowhere else. For the one case that rule cannot reach — a path that IS a
task's output and must still exist before wave 1 — the plan declares `scaffoldPaths`, and
`craft-dispatch.sh --scaffold` tells the guard to allow it. Without that list the only exit was
`--abandon`, which releases the guard for the **whole rest of the session** and silences the Stop
nudge along with it, so a plan defect became a disarmed run. A `readOnly` run never writes, so
there the Stop nudge is the only thing that fires. Derive the prose Run sizing block from the JSON;
`craft-dispatch.sh` prints the fan-out it computes, so drift shows up before anything is dispatched.

The user edits the plan file and approves via ExitPlanMode. Then **hash the plan where plan mode
actually wrote it** — resolve the path, don't assume it:

```bash
# The configured location — plansDirectory, project-root-relative, default .claude/plans.
PLANS=$(jq -r '.plansDirectory // empty' .claude/settings.local.json .claude/settings.json \
  ~/.claude/settings.json 2>/dev/null | head -1); PLANS=${PLANS:-.claude/plans}
PLAN=$(ls -t "$PLANS"/*.md 2>/dev/null | head -1)
[ -n "$PLAN" ] || PLAN=$(ls -t ~/.claude/plans/*.md | head -1)   # fallback: a pre-setting session
bash ~/.claude/skills/workflows/skills/craft/scripts/craft-dispatch.sh --spec-hash "$PLAN"   # 64-hex spec hash
```

Whichever it resolves to is `planPath`. **Never copy the plan** — one file, hashed in place, is the
run's authority. A copy creates a second file that can drift from the one the user edits. (The run
dir's `plan-<hash12>.md` is not that: it is a dead snapshot named by its own hash, written by
dispatch and never read as authority, so nothing can edit it or drift from it.)

**Ensure `plansDirectory` is set.** Plans belong inside `projectDir`, alongside the work and the
agents. `plansDirectory` is [relative to the project
root](https://code.claude.com/docs/en/settings), so any project-relative value puts them there —
`"./.claude/plans"` and `"./.planning"` both work, and craft resolves whichever is set:

```bash
rg -n '"plansDirectory"' .claude/settings.local.json .claude/settings.json ~/.claude/settings.json 2>/dev/null
```

If no tier sets it, add one — `"plansDirectory": "./.claude/plans"` is this skill's default,
`"./.planning"` is what the domain workflows use — to the project's
`.claude/settings.json`, and gitignore that directory. Setting it at the user tier covers every
project at once, which is usually what you want. Precedence is Claude Code's own:
`.claude/settings.local.json` beats `.claude/settings.json` beats `~/.claude/settings.json`.

**It takes effect next session, not this one.** Plan mode fixes the plan's path when you enter it, so
a session that started before the setting was live still writes to `~/.claude/plans/` — which is why
the snippet above resolves the real path instead of asserting one. Do not stop the run over it, and
do not copy the file to make the path look right.

The plan's `craft:dispatch` spec block is the sole authority every dispatched agent gets; the prose
around it explains but never binds. Nothing re-derives it: the agents re-run `--spec-hash` themselves
and stop on mismatch, so an amended spec halts the run instead of silently changing the contract,
while fixing a typo in the rationale costs nothing.

## Phase 3 — GOAL

**Self-send a `/goal`.** No file — the plan holds the success criteria and it's what's hashed.
What `/goal` adds is mechanical: after each turn a separate evaluator model checks the condition,
and if it doesn't hold **the session starts another turn instead of returning control to the user**.
That is what runs craft's outer loop (gate FAIL → fix → re-run; tuicr findings → fix → re-review)
without the user prompting each step. `workflow.js` can't do this — it returns a verdict once.

**`craft-dispatch.sh` does Phase 3 and Phase 4 in one call** — it reads the armed plan's dispatch
block, injects `planPath`/`specHash`, writes `args.json`, self-sends the goal below, and starts
`workflow.js` detached. Run it and skip to the Monitor; the rest of these two phases is what it
does and why, and what to check when it reports something odd:

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/craft/scripts/craft-dispatch.sh                    # armed plan; or pass one
bash ${CLAUDE_PLUGIN_ROOT}/skills/craft/scripts/craft-dispatch.sh --provider codex   # "run craft through codex"
```

**The provider comes from the invocation, and every skill that wraps craft forwards it.** A provider
named in `$ARGUMENTS`, however it is spelled — `--provider codex`, `--dispatch codex`, "run this on
gpt" — is `--provider codex` on the dispatch line, whether craft was invoked directly or through `/dev`,
`/ds`, `/writing`, `/notes`, `/slides` or `/exams`. Those skills print their own dispatch recipe, so
each carries the flag too; dropping it silently runs the user's codex request on claude.
`--provider` is the ONE spelling: `craft-dispatch.sh --dispatch` and `craft-redispatch.sh --dispatch
codex` each exit 2 naming it, rather than dying on "unknown flag" or accepting a synonym.

**`--provider claude|codex|gemini` (default `claude`) runs the WHOLE spine on that provider** — it
reaches `farm.sh --provider`, whose wrapper remaps the tier names, so every `model: 'sonnet'` in
`workflow.js` follows with no arg change. Same flag on `craft-redispatch.sh`, which is where it earns
its keep: when a round repeats its predecessor's failure exactly, a different provider is the lever
for framing lock-in (`references/convergence.md`). Whole-run granularity is structural — the provider
is chosen before `workflow.js` runs, so implementers and lenses cannot differ. It is deliberately not
written to `args.json`: differing between rounds is the point.

It needs nothing from the session's context, which is the point: a run whose context was cleared at
plan approval is recovered by this one command, with no re-exploration.

**The plan-review gates run before `args.json` is written, exiting 3 with the run still armed and
every artifact byte-identical.** Tier 1 is `plan-lint.ts` on the built args. The two probe gates
execute disjoint command sets, each command exactly once: **tier 2 runs every active task's
`redCommand`** through the script's own classifier, refusing `red-not-red` (exit 0 — the gate already
passes) and `could-not-run` (exit 127, a missing runner, `pytest` exit 4/5, or no test output at
all), so only non-zero *with* a real test result proceeds; **tier 2b runs every `mechanicalChecks`
cmd** at baseline via `plan-preflight.ts --only mechanical`, where only a `critical` refuses.
Acceptance commands are `plan-preflight`'s third probe kind and **no dispatch gate runs them** — run
`--only acceptance` by hand on a quiet tree, before arming.
`--no-red-probe` drops tier 2, `--no-mech-probe` drops tier 2b, `--no-lint` drops all of them, and
`CRAFT_RED_PROBE_TIMEOUT` / `CRAFT_MECH_PROBE_TIMEOUT` (300s each) bound their own tier's commands.
A task whose work is already COMPLETE can satisfy neither gate —
a `redCommand` is refused `red-not-red`, omitting it is refused `redcommand-missing` — so it declares
`redDisposition` instead; both scripts echo `red: N gated, M dispositioned` with each disposition,
beside the wave graph.

```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/craft/scripts/goal-self-send.sh \
  '/goal workflow.js has returned PASS for .claude/plans/<slug>.md at its current hash, and the tuicr gate has returned approved, or stop after N turns'
```

**Name the plan by PATH, never by a fixed sha256.** A pinned digest self-invalidates the first time
the FAIL loop does what this file prescribes: fix, **amend the plan, re-hash**, re-dispatch. The run
then PASSes against a hash the condition does not name, and the evaluator correctly reports the goal
unmet on finished work. The path is stable; the hash is the thing the loop is expected to change.

**Write the condition so the transcript can prove it.** The evaluator judges only what has been
surfaced in the conversation — it runs no commands and reads no files. "The acceptance criteria
hold" is unjudgeable; "the gate returned PASS and tuicr returned approved" is judgeable, because
both verdicts get printed. Include a turn clause to bound the run.

**On a `readOnly` run the condition must not name PASS.** Phrase it as *"workflow.js has returned a
verdict for `<planPath>` and the tuicr gate has returned approved"* — satisfied by either
verdict. A PASS-conditioned goal would keep starting turns driving toward an outcome the run is
forbidden to produce, because the only way to turn an audit's FAIL into a PASS is to fix what it
found, and a read-only run writes nothing.

Two mechanics worth knowing: setting a goal **starts a turn immediately** (that turn is Phase 4),
and only one goal can be active per session.

**Send it as the last action of the turn, then stop.** The message lands in *our own* input queue
and cannot be processed until this turn ends — so there is nothing to wait for, and blocking on it
would deadlock. Do **not** dispatch Phase 4 in the same turn: the goal would be set after the gate
already ran. Let the turn end; the `/goal` turn is Phase 4.

**Exit 0 means submitted, not processed.** The script sends the paste and the Enter separately, then
watches the pane's input line: a [swallowed Enter](../agent-spawn/references/prompt-delivery.md)
leaves the text sitting there, so it retries — **bounded** at three presses over about five seconds,
then exit 5 saying the text is still in the box. It refuses outright (exit 6) if the box is non-empty
when it starts, the guard against pasting into a message the user is mid-way through typing.
Transports are tried in order: our own herdr pane, then `agent-msg` for Remote-Control sessions.
Refusals are fail-closed — every non-zero path exits **before** anything is sent, except the two
"send attempted and failed" cases (exit 5).

Whether the queued line is *acted on* is unknowable from here — that needs the turn to end. Exit
non-zero (no transport, or the Enter never took) is **not fatal**: the loop is written down here,
it just needs the user to prompt each step. Clear on final approval:
`goal-self-send.sh '/goal clear'`.

If any of this is in the way, print the `/goal …` line and let the user submit it. One keystroke,
no race, no ordering constraint.

## Phase 4 — workflow.js

The args, annotated — write them to the args file as **plain JSON**, no comments, since the workflow
`JSON.parse`s it:

```js
{
  projectDir, planPath: "<the path $PLAN resolved to in Phase 2>", specHash: "<64-hex>",
  goal: "<one sentence>",
  tasks: [{id, name, work, writablePaths, acceptance, refs, redCommand|redDisposition}, ...], // from the plan's task table
  thirdParty: ["codex"],          // ONLY if the plan carries the opt-in line; else omit
  mechanicalChecks: [{name: "node-check", cmd: "node --check foo.js"}, ...],  // optional
  scoredChecks: [{key, items, prompt, schema, components, passthrough, refs, agentType}, ...], // optional; advisory, never gates
  reviewLenses: [{key, prompt, refs, agentType}, ...],            // optional; default is 2 lenses
  // Standard, and the one to carry on any multi-task plan. `craft-dispatch.sh` PRINTS the wave shape
  // `dependsOn` produces; this asks whether it has to be that shape. MAJOR-capped, so it never blocks:
  // {key: "plan-parallelism", agentType: "Explore", refs: [],
  //  prompt: "Judge ONLY the approved plan's dependsOn edges. MAJOR at most, never CRITICAL — this is advisory. Given each task's `work`, `writablePaths` and `refs`: is any dependsOn edge unnecessary — could the two tasks run in the same wave? One finding per edge you would remove, naming the dependent, the dependency, and what the dependent actually needs from the dependency; if that is a BEHAVIOUR (an exit code, an observable effect) rather than a file the dependency writes, say so — an edge is a READ ordering, so a behaviour need is not one. Be specific or silent: a finding that names no concrete pair and no concrete reason is not a finding, and 'consider parallelising' is not one. Never argue for removing a test-first edge — the task that writes a failing test before the task that makes it pass — collapsing that means writing a test beside its fix, which is exactly what red-gating exists to prevent. Report nothing if every edge is load-bearing."},
  authorityExtra: "<domain rule appended to every agent's AUTHORITY block>",  // optional
  implementerAgentType: "…", verifierAgentType: "Explore",        // optional
  readOnly: true,                                                 // optional; audit an existing tree
  priorFindings: [{title, severity, detail, file, lens}, ...],    // optional; discoveries made outside this run
  freezeFindingSet: true, maxRounds: 3,                           // optional; set by craft-redispatch.sh, not by hand
  maxAgents: 50, refutersPerLens: 8,                              // optional; fan-out ceilings — throws if the floor exceeds maxAgents
  refuterModel: "sonnet", refuterEffort: "medium",                // optional; null on either inherits the session default
}
```

**Dispatch through farm-out, never the built-in `Workflow` tool** — the guard at
`~/.claude/hooks/main-thread-guard.sh` denies it unconditionally, so an in-session call is
dead. `farm.sh` sets `FARM_OUT_CHILD=1`, which is what lets its child make the call.
`craft-dispatch.sh` runs exactly this and prints the wait loop with the run's paths filled in; what
follows is what it does, for when you are reading its output or a dispatch has gone wrong.

```bash
R=.craft/<run-id>; mkdir -p "$R"          # the runner refuses if --out's directory does not exist
# write the args object above to "$R/args.json" as JSON
# DETACHED, never foreground: a real gate runs 20-60 min and a foreground tool call caps out
# and kills it mid-run, as does a harness-tracked background task.
setsid nohup bash ${CLAUDE_PLUGIN_ROOT}/skills/farm-out/scripts/farm.sh \
  --workflow ${CLAUDE_PLUGIN_ROOT}/skills/craft/workflow.js \
  --args "$PWD/$R/args.json" --out "$PWD/$R/result.json" --cwd "$PWD" \
  > "$R/run.log" 2>&1 < /dev/null &
# Wait for it: craft-result.sh is one-shot and knows nothing about the dispatch. Watch BOTH the
# artifact and the process — a watcher that only greps for success is silent through a crash — and
# key liveness on THIS run's --out path, or a concurrent dispatch reads as proof ours is alive.
# farm-alive.sh keys on the RUN, not the runner's filename: the runner's event file carries
# out=<this run's --out> and its filename is the pid, so a rename cannot break the check.
while :; do
  [ -s "$PWD/$R/result.json" ] && break
  bash ${CLAUDE_PLUGIN_ROOT}/skills/craft/scripts/farm-alive.sh "$PWD/$R/result.json" > /dev/null \
    || { echo "dispatch died with no verdict — see $R/run.log" >&2; exit 1; }
  sleep 30
done
bash ${CLAUDE_PLUGIN_ROOT}/skills/craft/scripts/craft-result.sh "$PWD/$R/result.json"
```

**`--loops N` executes that wait instead of printing it.** After dispatch `craft-dispatch.sh --loops
N` hands the run to `craft-loop.sh`, which polls with the liveness leg above, reads the verdict,
consults `converge-check.ts` every round, and redispatches to a cap of N rounds. N defaults to the
args `maxRounds` value, or 3 when absent; a non-numeric value is refused with exit 2 naming the flag.
`--loops 0` is the printed path unchanged — the wait loop above prints and the script exits 0.

The driver's exit codes: **0** PASS; **exit 1** the dispatch died with no verdict (see the run log it
names); **exit 2** `craft-result.sh` refused the verdict; **exit 5** `converge-check.ts` reported NOT
CONVERGING, which is evidence about the BRIEF — re-plan rather than spend the remaining rounds;
**exit 6** the round cap was reached; **exit 7** a plan defect needs a scope decision, which is a
human's to make.

**Run that wait as a `Monitor`, not a foreground Bash call** — Bash caps at 10 minutes and a gate
runs 20-60. Pass the loop body as Monitor's `command` with `persistent: true` (no deadline), keeping
both terminal states: result written, and process gone without one. Then call `craft-result.sh` when
it fires. Fall back to the loop across turns only where Monitor is unavailable — Bedrock, Vertex,
Foundry, or `DISABLE_TELEMETRY`/`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` set. A Monitor dies with
the session; the detached run does not, so `/goal` remains the notifier that survives a restart.

`farm.sh` exits 2 on a malformed call (`--workflow` without `--out`, an `--args` file that is not
readable JSON) and non-zero when `--out` came back missing or not a JSON object. `craft-result.sh`
then refuses (exit 2) unless the file is one object carrying `overallPass`, `verdict`, `scoreTable`,
`findings`, `tasksThatFlagged`, `mechanicalThatFailed` and `lensesThatFlagged` with the right types,
and prints the verdict and the score table on success. All three selectors are required: a return
dropping one channel would make a FAIL carried solely by that channel read as a clean run.

**State the residual plainly: mechanical claims are adjudicated, the rest is shape, not fidelity.**
A model transcribes the workflow's returned object into `--out`, so a fabricated object with the
right keys — outside the re-run mechanical claims — passes both checks. Reconcile the score table
against the plan's Run sizing: counts that cannot be squared with what the run was sized to dispatch
make the result unverified, not a PASS.

| param | type | effect |
|---|---|---|
| `readOnly` | `boolean` (default `false`) | Audit mode. **No Implement phase and no per-task verifiers are dispatched**, so `tasks[]` may be empty or absent (it is still required when `readOnly` is false). Lenses ∥ mechanical ∥ third-party run as usual, and **every dispatched leg** — lenses, refuters, mechanical probes and third-party runners — defaults to the `Explore` agent type, structurally no Edit/Write, unless a per-lens `agentType` says otherwise. `Explore` keeps `Bash`, so a probe can still run its command. **Residuals**, all from `Bash`, and this list is open rather than exhaustive: a `mechanicalChecks` `cmd` runs VERBATIM; any reference this spine tells a leg to follow can itself instruct a write; and `authorityExtra` and `reviewLenses[].prompt` are caller-supplied free text handed to every Bash-capable leg. What the agent type pins is the agent's volition — never what it is *told* to do. Anything a `readOnly` run hands a leg must itself be read-only. `meta.phases` is a **static five-entry literal** (`Implement, Verify, Mechanical, Third-party, Gate`) in both modes — the harness parses `meta` without running the script and rejects any computed value, so a mode-specific phase list is not expressible. `Implement` is therefore advertised and then never opened on a `readOnly` run; that is `workflow.js`'s own progress display and is cosmetic. Craft's lifecycle **Phase 1–5** (CLARIFY, PLAN, GOAL, workflow.js, HUMAN REVIEW) is a different axis and is unaffected. The task dimensions become **n/a** (`null`), not empty-and-clean: see the score-table note below. |
| `priorFindings` | `[{title, severity, detail, file?, lens?}]` | Findings discovered **outside** this run — typically by a main-chat agent team. They are not trusted: each is refuted by the same adversarial path a lens finding takes (same schema, same default-to-refuted-when-ambiguous, same fail-closed rule that a dead refuter keeps the finding), and only survivors reach the gate, where they are gated identically to lens findings. An entry with no `lens` is attributed to the reserved key `unattributed` — the same key an unkeyed `reviewLenses` entry falls back to, since it is the same situation — so a survivor always names a `lensesThatFlagged` entry. Set `lens` explicitly to get a more specific label. **`file?` is the path the finding is _about_** — it is rendered into the refuter's prompt as `[path]` for context and is never opened by craft. It is **not** a location craft reads findings from; for where a team actually writes them, see *Where the agent team lives*. `severity` must be `critical｜major｜minor`; a malformed entry throws at arg-validation, before any agent is dispatched. An optional `agentType` on an entry overrides the refuter's agent type for that finding alone. **Each entry costs one refuter agent**, and the fan-out is bounded by nothing but the array you pass — so the count feeds the ~50-agent ceiling below. A `minor` entry cannot change the verdict (only `critical｜major` reach `survivingBlocking`), so it spends a full agent to move a display counter: submit `critical`/`major` unless you specifically want the minor counted. `scoreTable` then carries `priorFindingsSubmitted` / `priorFindingsSurviving`. |
| `mechanicalChecks` | `[{name: string, cmd: string}]` | Adds a `Mechanical` phase running in parallel with Verify. One low-effort probe agent per check runs `cmd` **verbatim** and reports `{name, exitCode, output}`; **the JS reads the exit code** — no agent asserts a pass. Fail closed: a dead or skipped probe is `exitCode: -1`, which counts as failed. Missing `name` or `cmd` throws. **A probe's report is still a claim, and the claim is adjudicated in a shell**: craft-result.sh re-runs EVERY declared check — a claimed failure included, since the file is a model's transcription of the gate object — and refuses (exit 2) when the observed exit code disagrees, or when a claimed non-zero exit sits beside `overallPass: true`. The refusal names **which direction** the disagreement went, because they mean opposite things: a claimed FAILURE that passes on re-run is a probe-side flake — re-run the gate, do not re-plan — while a claimed PASS that fails on re-run is the case the adjudicator exists for. Both still exit 2; passing a non-reproducing failure would wave a genuinely flaky gate through. Re-running one command to confirm a claim is cheap and re-running N is not — which is why a workflow declares **one** mechanical entry point whose exit code is its whole mechanical verdict, never a list of commands (a list also drops a check silently, and nothing reports a check it never knew about). **A check's `cmd` must finish inside ~10 minutes, because it is run TWICE by two different callers that both cap there**: a probe agent runs it through its Bash tool (hard ceiling 600s, not tunable) and `craft-result.sh` re-runs it to adjudicate the claim. A check that outlives that returns 124/137/143 — a kill, not an exit — which scored as a *failing gate* until craft-result.sh learned to refuse it. Anything genuinely long (a scale run, a soak) goes **behind** the gate, not inside it: run it detached (`run_in_background`, or a `Monitor` when you want per-event notice) writing an artifact, and let `cmd` be the fast read of that artifact. **`overallPass` in `result.json` is NOT the verdict and must never be read directly, without exception** — the verdict is `craft-result.sh`'s exit code (0 pass, 1 fail, 2 refused), and any caller that reads the file another way is a defect. |
| `scoredChecks` | `[{key, items, prompt, schema, components, refs, agentType?}]` (default off) | Weighted 0–10 scores, one agent per `items` entry, running in parallel with Verify. **The agent returns RAW COUNTS and craft computes every score in JS** from the caller-declared `components`, so no agent ever sees the formula it is scored by — an agent that reports its own score inflates it. **It is advisory and structurally cannot gate**: `overallPass` is computed without reading any scored value, there is no threshold and no `blockBelow`, it adds no selector channel, and even a dead agent does not flip the verdict. An unmeasured, dead or partially-reported item scores `null` with a reason — never `base`, never `0`. Absent or `[]` opens no phase and dispatches nothing, and the return then carries `scores: []` with `scoresRun`/`scoresReported` as `null` (n/a — render it as such, never `0`). A schema key that is not a declared count, a score-shaped name, or a `penalties` key the schema does not declare all throw at arg-validation, before any dispatch. **`passthrough: [<field>, …]`** declares the evidence a score never reads — the numeric denominators a finding is stated against and the item lists it is built from — which a count-only whitelist cannot express; it stays a whitelist (an undeclared field is still refused, a field cannot be both a penalty and passthrough, and the score-shaped-name check applies to it too). Declared fields come back on that item's entry under **`evidence`** — nested, so nothing can collide with a component name; absent entirely when none was declared or reported; present on a `null`-scored item and never on a dead agent. Each item counts against `maxAgents`. Contract, arithmetic and worked example: [`references/scored-checks.md`](${CLAUDE_PLUGIN_ROOT}/skills/craft/references/scored-checks.md). |
| `tasks[].redCommand` | `string` (optional) | Test-first gate that is **executed**, never asserted: a probe agent runs the string verbatim before the implementer — the JS requires a **non-zero** exit — and a second probe runs it after, where the JS requires **zero**. Three failure verdicts, each fails the task and puts its id in `tasksThatFlagged`: `red-unproven` (a probe died or was skipped, `exitCode: -1`), `red-not-red` (exit 0 before, so the test proves nothing), `green-not-green` (non-zero after). Must be **one invocation** — the shell operators `` ; & \| ` $ > < ( ) { } `` and newlines throw at arg-validation, because the probe runs the string with its own authority and a shell program can fabricate RED; flags and quotes are fine, a multi-step check goes in a script you name. Costs **2 agents** against `maxAgents`; no probe is dispatched under `readOnly`. `scoreTable` then carries `redGated`, `redProven`, `redUnproven`, `redNotRed`, `greenNotGreen`, and the return carries `red` — feed it back as `priorResults.red` so a carried task keeps its adjudication instead of re-reading as unproven. It does **not** close everything: the command loads code the implementer may control, so keep `writablePaths` narrow. Absent leaves every existing caller byte-identical. |
| `tasks[].redDisposition` | `string` (optional) | The filed reason a task carries **no** red gate — for work already complete, where any `redCommand` would be refused `red-not-red`. plan-lint accepts it INSTEAD of `redCommand` (both declared is `red-both-declared`, MAJOR; neither is `redcommand-missing`, MAJOR; empty/whitespace reads as absent), and dispatch echoes it verbatim. **Its content is never validated** — non-empty is the whole check; grading prose is the non-terminating shape. Inert to workflow.js: no probe, no agent, no score field. |
| `scaffoldPaths` | `string[]` (optional) | Paths the plan authors **before** the dispatch, even though a task also writes them. Read only by `main-thread-guard.sh` via `craft-dispatch.sh --scaffold` (0 declared, 1 not, 2 undecidable → the guard fails closed); it changes nothing about how implementers run, and `writablePaths` still governs who may write what during the run. Exists for the greenfield red gate: a `redCommand` on a surface that does not exist yet fails to import, which is `could-not-run`, so a stub has to be on disk before wave 1 — and a stub is the implementer's output, so `--covers` alone can only deny it. Keep it to the specific file: a scaffold covering a task's whole writable surface is `scaffold-swallows-task` (major) at plan-lint. Absent leaves every existing caller byte-identical. |
| `tasks[].dependsOn` | `string[]` (task ids, optional) | A **read ordering**: declare it when this task's `refs`, tests or inputs are files another task writes. IMPLEMENT then runs in waves — concurrent within a wave, waves in order. **Absent everywhere leaves every existing caller byte-identical**: one wave, `tasks[]` order. Refused at arg-validation, before any dispatch: a non-array or self-referencing value, an **unknown id** (a typo would silently drop the ordering it was written to enforce), a **cycle** (named with every id in it — a cycle means two tasks each need the other's output), and a wave whose tasks claim **overlapping `writablePaths`** (prefix-aware). An edge to a task outside `onlyTasks` is **satisfied, not unschedulable** — a prior run implemented it and its output is on disk, so refusing it would make every scoped re-run impossible. A `redCommand` still brackets its own implementer inside a wave, never a sibling's. |
| `tasks[].refs` | `string[]` (absolute paths) | Files the implementer must Read in full before working. Absent or `[]` injects nothing into the prompt. |
| `reviewLenses[].refs` | `string[]` (absolute paths) | The rules the lens judges against. **The lens is told to read them IN FULL; its refuters are only told the paths**, with an instruction not to open them unless the finding's own quoted evidence is insufficient (and to say so if they do). A lens is one agent doing open-ended reading; refuters are one agent *per finding*, so handing each the full ref set multiplies the run's largest read by the finding count. Absent or `[]` injects nothing either way. |
| `maxAgents` | `number` (default `50`) | Hard ceiling on the fan-out floor, checked at arg-validation. **Throws before any agent is dispatched**; the error names each dimension's count. Raise it deliberately, in the plan — see the sizing note above. |
| `refutersPerLens` | `number` (default `8`) | Cap on refuters dispatched per lens — the one fan-out term 

…(truncated)
