diff --git a/docs/flow.svg b/docs/flow.svg index 2805e78..5f3b939 100644 --- a/docs/flow.svg +++ b/docs/flow.svg @@ -33,6 +33,12 @@ + + + spec-prep + optional pre-flight — hardens the spec first + + ingest diff --git a/install.sh b/install.sh index 204ead2..529d5df 100755 --- a/install.sh +++ b/install.sh @@ -29,8 +29,10 @@ echo "✓ spec-prep -> $BIN_DIR/spec-prep" if [ -d "$HOME/.claude" ]; then mkdir -p "$HOME/.claude/skills" - ln -sfn "$HOME_DIR/skills/millify" "$HOME/.claude/skills/millify" - echo "✓ millify skill -> ~/.claude/skills/millify" + for skill in millify drive-mill; do + ln -sfn "$HOME_DIR/skills/$skill" "$HOME/.claude/skills/$skill" + echo "✓ $skill skill -> ~/.claude/skills/$skill" + done fi command -v conductor >/dev/null 2>&1 || cat <<'MSG' diff --git a/site/content/concepts/design-rules.md b/site/content/concepts/design-rules.md index 5020979..9cec035 100644 --- a/site/content/concepts/design-rules.md +++ b/site/content/concepts/design-rules.md @@ -2,7 +2,7 @@ title: Design rules description: The invariants that make mill runs trustworthy. group: Concepts -order: 6 +order: 7 --- Each rule here exists because its absence produced a concrete failure — @@ -17,6 +17,15 @@ Universal invariants over existing code must carry transition rules, or they are unsatisfiable incrementally and two honest models will spend six rounds proving it. +What blocks is severity, not count. An adversarial reader of a real codebase +can almost always name one more edge case, so "zero findings" is an asymptote +— demanding it just trades six rounds of plan review for six of spec review. +The bar is *no blocking or high-severity defect*; medium and low findings are +recorded as accepted interpretations and carried into planning, where three +downstream review gates still cover them. The optional +[`spec-prep`](/concepts/spec-prep) pre-flight applies exactly this bar to +harden a spec to a millable state before a run starts. + ## Scripts own control flow Loop counters, gate results, and every git operation live in diff --git a/site/content/concepts/how-it-works.md b/site/content/concepts/how-it-works.md index b353683..6278d5b 100644 --- a/site/content/concepts/how-it-works.md +++ b/site/content/concepts/how-it-works.md @@ -27,9 +27,14 @@ doesn't have? Does it contradict itself — especially universal invariants ("every", "always", "never") that current code doesn't satisfy and that lack a transition rule? Is it ambiguous in ways that change the work? Is it small enough to converge (roughly a dozen chunks)? Findings escalate to a human -immediately; in `--auto` runs they abort. Fix the spec at its source and -rerun — that's cheaper than discovering the same defects through six rounds -of plan review. +immediately; in `--auto` runs they abort. Fixing the spec at its source is +cheaper than discovering the same defects through six rounds of plan review. + +For a spec that needs real work first, [`spec-prep`](/concepts/spec-prep) is +an optional pre-flight that hardens it to a millable state in its own cheap +loop — resolving blocking defects against source truth and converging on "no +blocking findings" rather than the unreachable "zero findings" — so the run +below clears this gate on the first pass instead of aborting and rerunning. ## Plan, adversarially reviewed diff --git a/site/content/concepts/self-improvement.md b/site/content/concepts/self-improvement.md index 796ece0..86fe949 100644 --- a/site/content/concepts/self-improvement.md +++ b/site/content/concepts/self-improvement.md @@ -2,7 +2,7 @@ title: Self-improvement description: How runs turn friction into policy. group: Concepts -order: 7 +order: 8 --- Mistakes should be made once, then become policy. The mill closes that loop diff --git a/site/content/concepts/spec-prep.md b/site/content/concepts/spec-prep.md new file mode 100644 index 0000000..8b4c182 --- /dev/null +++ b/site/content/concepts/spec-prep.md @@ -0,0 +1,92 @@ +--- +title: Hardening a spec first +description: The optional pre-flight that makes a spec millable before a run spends a token. +group: Concepts +order: 6 +--- + +The mill validates the spec at the start of every run. A spec with real +defects fails that gate — and because the reviewer reads *source truth*, a +large spec can surface a *different* defect on each pass: you fix one, rerun, +and the next round finds the next. Fixing at the source and rerunning the +whole pipeline each time is correct but slow, and it drags a human through +one decision at a time. + +`spec-prep` moves that hardening into its own cheap loop that runs to +convergence *before* the implementation mill starts. It has no worktree and +no baseline — it only reads the code and rewrites its own copy of the spec — +so looping is fast, and the mill it feeds sails straight through the spec +gate it would otherwise park at. + +## The loop + +![The spec-prep pre-flight loop: a spec is reviewed against source truth, +then a severity gate either passes it through as a hardened spec into the +mill (no blocking findings), sends it to a Claude hardener that resolves +blocking findings and loops back to review, or stalls when the round budget +is spent and a decision is needed.](/spec-prep.svg) + +A rival model reviews the spec against the code, exactly as the mill's own +spec gate does. Then a **hardener** — a strong model with repository access — +rewrites the spec to resolve each finding, grounded in the actual files and +existing precedent, and the loop reviews again. It repeats until the review +comes back clean or a round budget is spent. + +## Severity decides what blocks + +The key move — and the reason the pre-flight converges where a bare rerun +loop doesn't — is that not every finding blocks: + +- **blocking / high** findings must be resolved. A false claim about the + code, a genuine contradiction, an ambiguity that changes the shape of the + work — the spec can't be implemented correctly until these are fixed. +- **medium / low** findings are recorded as *accepted interpretations* and + carried into planning, not treated as blockers. + +Demanding *zero* findings from an adversarial reader of a real codebase is an +asymptote it never quite reaches — there is almost always one more +serve-time edge case to name. And three more review gates sit downstream in +the mill (plan review, per-chunk review, final compliance). So the pre-flight +converges on "no blocking defects remain," records the rest, and lets the +pipeline's later gates catch residual depth. + +## The hardener resolves; it doesn't guess + +For a finding that source truth settles — a wrong file reference, a schema +that should follow an existing type — the hardener corrects the spec and +cites the real code. For a genuine *product* decision that the code can't +settle — expose a UI or stop at the API, add fixtures or test synthetically — +it picks the most conservative, smallest-scope reading, writes the spec to +that reading so it is unambiguous, and records the decision in +`.mill-prep/spec_decisions.json` for a human to review or override. + +To decide those forks yourself up front, put authoritative answers in +`.mill-prep/spec_answers.md` before running; the hardener honors them exactly +rather than choosing. Either way, the finished spec carries a **Spec-prep +record** — every interpretation and decision made — so nothing is silently +chosen. + +## Running it + +```sh +spec-prep 58 # harden issue #58 +spec-prep 58 --web # with the live dashboard +mill .mill-prep/spec.hardened.md --no-pr # then implement the hardened spec +``` + +| Flag | Effect | +| --- | --- | +| `--web` | Detach into the background with the live web dashboard. | +| `--fresh` | Discard existing `.mill-prep` state and start over. | +| `--dest=PATH` | Where to write the hardened spec (default `.mill-prep/spec.hardened.md`). | + +The output is a spec file — with a title header, so a file-sourced mill run +derives a good PR title — that the mill consumes directly. If the source +issue was already sound, `spec-prep` finalizes on the first review with +nothing to change; if it can't converge within the round budget, it stalls +with the unresolved findings, which usually means a product decision is +waiting for you in `spec_answers.md`. + +State lives in its own `.mill-prep/` directory (self-gitignored), so +`spec-prep` never collides with an implementation run's `.mill/` in the same +checkout. See [running concurrently](/reference/concurrency). diff --git a/site/content/reference/cli.md b/site/content/reference/cli.md index af78c1b..bf39459 100644 --- a/site/content/reference/cli.md +++ b/site/content/reference/cli.md @@ -6,11 +6,13 @@ order: 20 --- ```sh -mill [--auto] [--web] [--no-pr] [--no-deep] [--fresh] +mill [--auto] [--web] [--no-pr] [--no-deep] [--fresh] [--model=M] ``` The argument is either a GitHub issue number (fetched with `gh` from the -current repository) or a path to a markdown spec file. +current repository) or a path to a markdown spec file. To harden a rough spec +before running, see [`spec-prep`](/concepts/spec-prep), whose hardened output +is a spec file you pass here. | Flag | Effect | | --- | --- | @@ -19,6 +21,7 @@ current repository) or a path to a markdown spec file. | `--no-pr` | Never push or open a PR; the branch stays local regardless of the ship decision. | | `--no-deep` | The final gate re-runs the fast chunk gates instead of `[gates].deep`. | | `--fresh` | Discard an existing worktree and branch for this source and start over. | +| `--model=M` | Model for the implement/fix agents (default `sonnet`); bump to `opus` for reasoning-heavy chunks like parsers and contract tests. Review models are fixed. | ## Where things live diff --git a/site/content/reference/concurrency.md b/site/content/reference/concurrency.md index dd44698..35a2999 100644 --- a/site/content/reference/concurrency.md +++ b/site/content/reference/concurrency.md @@ -24,6 +24,12 @@ worktree** (same id, same repo): a live conductor already using that worktree as its cwd blocks the launch, so you can't corrupt an in-flight run — not even with `--fresh`. +[`spec-prep`](/concepts/spec-prep) runs in the repo root rather than a +worktree, so it keeps its state in its own `.mill-prep/` directory (never an +implementation run's `.mill/`) and refuses a second `spec-prep` in the same +checkout. A `spec-prep` and a `mill` run can execute together, and `spec-prep` +runs in different checkouts never contend. + ## What the mill does NOT isolate The mill isolates the filesystem and git. It does **not** give each run its diff --git a/site/public/flow.svg b/site/public/flow.svg index 2805e78..5f3b939 100644 --- a/site/public/flow.svg +++ b/site/public/flow.svg @@ -33,6 +33,12 @@ + + + spec-prep + optional pre-flight — hardens the spec first + + ingest diff --git a/site/public/spec-prep.svg b/site/public/spec-prep.svg new file mode 100644 index 0000000..781f0cd --- /dev/null +++ b/site/public/spec-prep.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + spec + issue / file + + + review + vs source truth + + + gate + by severity + + + hardened spec + .mill-prep + + + the mill + implements + + + + + no blocking + + + + + harden + resolve blocking + + + + blocking? + + + re-review + (bounded) + + + + stall + a decision is needed + + budget spent + + + + script — referees severity + + GPT — reviews + + Claude — hardens + + bounded loop + diff --git a/skills/drive-mill/SKILL.md b/skills/drive-mill/SKILL.md new file mode 100644 index 0000000..c3edb76 --- /dev/null +++ b/skills/drive-mill/SKILL.md @@ -0,0 +1,172 @@ +--- +name: drive-mill +description: Operate the frostyard mill (spec→PR harness) to take a GitHub issue or spec through to a branch — decompose an umbrella issue into phases, harden each spec, launch runs, read the gate signals, and handle clarify parks, deadlocks, and concurrency safely. Use when the user asks to run/drive the mill, take an issue through the mill, run a phase, or kick off a mill run. +--- + +# Drive the mill + +This is the operator playbook for running the mill against a repository's +issues. The mill itself is deterministic; *driving* it well is a discipline. +The failure mode is thrash — aborted launches, re-derived context, wasted +tokens — and almost every rule here exists to prevent one specific kind. + +Prerequisite: the repo has a `.mill.toml` (run the `millify` skill first if +not). Runs use the **installed** engine at `~/.local/share/frostyard-mill`, +not any repo clone — see [Engine drift](#engine-drift-hazard). + +## 0. The shape of a good run + +`spec-prep ` → hardened spec → `mill ` → spec gate passes on +the first try → plan → your approval → chunk loop → final reviews → branch. +When a run parks or deadlocks, it is almost always the **spec's** fault, not +the pipeline's. Fix the spec, not the run. + +## 1. Decompose an umbrella issue into phases + +A large feature issue is not one run. Break it into phases small enough to +converge — each a **single run of roughly ≤12 chunks**, in dependency order, +each landing on `main` before the next starts. A phase that honestly +estimates much beyond a dozen chunks should be split again. + +Phases are sequential for a reason: **later-phase specs drift from +merged-earlier-phase reality.** You cannot fully harden phase N+1 until phase +N is merged, because N+1 references code N ships. Re-grounding is irreducible +per-phase work; do not try to pre-harden the whole umbrella up front. + +## 2. Re-ground each phase before launching it + +Before every phase, verify its spec against the **currently merged** code — +not the code as it was when the issue was written: + +- `git fetch && git checkout main && git pull` so you compare against reality. +- Read the issue body. For every concrete claim it makes about the code — + a function, type, capability, flag, file — confirm it actually exists now + (grep/read it). Prior phases change these. +- Fix stale references at the source before running. This catches drift that + would otherwise surface as a spec-gate clarify on round one. + +## 3. Harden the spec with spec-prep (don't hand-edit in a loop) + +If the spec is underspecified or you expect defects, run the pre-flight +**before** the mill: + +```sh +spec-prep --web +``` + +It reviews against source truth, then a hardener resolves blocking/high +findings and loops until none remain (medium/low are recorded, not blocking). +It emits `.mill-prep/spec.hardened.md`. Genuine *product* decisions it can't +settle from code (expose a UI or not; add fixtures or test synthetically) are +logged to `.mill-prep/spec_decisions.json`; to decide them yourself first, +write authoritative answers to `.mill-prep/spec_answers.md` and rerun. + +Feed the result to the mill: `mill .mill-prep/spec.hardened.md`. If the issue +body already holds the hardened content, running `mill ` directly is +fine and gives a nicer issue-linked PR. + +**Do not** hand-edit the issue, rerun, hit the next clarify, hand-edit again, +four times over. That is exactly the thrash spec-prep exists to replace: an +adversarial source-reader can always name one more edge case, so chasing +"zero findings" by hand is an asymptote. Harden once, up front. + +## 4. Launch + +```sh +mill --auto --web --no-pr --model=opus +``` + +- `--auto` — unattended; auto-approves the human gates (safe options on + escalation). +- `--web` — background run with the live dashboard (note its port). +- `--no-pr` — keep the branch local; the user merges (see §8). +- `--model=opus` — use for reasoning-heavy phases (parsers, contract tests, + capability tables). Mechanical phases are fine on the default sonnet. + +Drop `--auto` for a first run on a new repo, so you can eyeball the plan. + +## 5. Read the signals + +Watch `.worktrees/mill-/.mill/journal.jsonl` and the dashboard: + +- **Spec gate** — `spec_review verdict: sound` → planning (the goal). A + `clarify` park means the spec still has defects (see §6). After a proper + spec-prep pass this should pass first try. +- **Plan size** — a plan of ≫12 chunks means the phase is too big; stop and + split it. +- **Plan-review trajectory** — the escalation reports whether objections are + *converging* (fix those few points), *repeating* (one reviewer/planner + disagreement — decide it), or *finding new sections each round* (spec too + large — decompose). +- **Chunk loop** — implement → gates → review → commit, bounded. The hardest + class is **contract-test and parser chunks**: budget a single opus resume + for them. + +## 6. Handle a clarify park + +Read `.mill/spec_findings.json`. Classify each finding: + +- **Source-groundable** (wrong reference, a schema that should follow an + existing type, a degraded-mode behavior the code already implies): resolve + it yourself against the code and fix the spec — this is what spec-prep's + hardener automates. +- **Genuine product decision** (scope, UI-or-not, fixtures-or-synthetic): + surface it to the human with a recommendation grounded in precedent. Do + not invent the answer. + +Prefer **fixing the spec at its source and rerunning** (or a spec-prep pass) +over "proceed anyway" — proceeding bakes the defect into the plan. If you +resolve findings by hand, verify each edit actually landed (issue bodies get +reflowed; a blind string-replace can silently no-op), then confirm with a +grep before relaunching. + +## 7. Handle a deadlock + +A chunk that exhausts its bounded retries stops with a resumable checkpoint +and a failure-harvest. The reliable recovery: read what harvest learned, +resume carrying the explicit objections, and — for the hard classes — bump +to opus. Do not paper over a red gate; a script decides pass/fail, and piping +a gate through `tail` to swallow its exit code is the exact sin the reviewers +flag. + +## 8. Finish + +With `--no-pr` the branch is local. Verify it (`git log`, tree clean), report +what shipped (chunks, files, security + compliance verdicts), and let the +**user** publish/merge — that is an outward action that is theirs to take. +When they ask you to publish, run the publish step from the worktree: + +```sh +cd .worktrees/mill- && python3 ~/.local/share/frostyard-mill/mill_state.py publish +``` + +The PR title is the spec's title verbatim, so ensure the spec title is a +valid conventional-commit subject (e.g. `feat: …`), or the squash-merge trips +commit-lint. + +## Concurrency safety (do not break other runs) + +The user may run **several mills at once, across different projects**. + +- **Never** `conductor stop --all` or `pkill -f conductor` — it is + cross-project and kills their other runs. Stop only *your* run, by its + dashboard **port** (`conductor gate respond --port

--choice abort` to + clear a parked gate) or its PID. +- Clean only the worktree you launched (`.worktrees/mill-`), never a + blanket sweep. +- The driver refuses a second run in the same worktree; honor that rather + than forcing `--fresh` over a live run. + +## Engine drift hazard + +Runs execute `~/.local/share/frostyard-mill/mill_state.py` (and `mill.yaml`), +re-loaded fresh on every script step — **not** any repo clone. So: + +- If a run misbehaves in a way the repo source doesn't explain, diff the + installed engine against the repo; they may have drifted. +- After editing the engine, sync the installed copy (`git pull` in it, or + reinstall) before relying on the change. +- **Do not edit the shared engine while other runs are in flight** — every + script step of every live run picks up your change mid-run. Additive + changes are usually safe; behavioral ones are not. Wait for the batch to + drain, or flag it to the user first.