Grinder — autonomous looped repo work
Self-driving repo workflow. Plan a work unit, council the plan, revise, implement, test, council the staged diff, commit. Repeat. Wake-up between iterations is driven by background-task completion notifications. State survives auto-compact via Claude Code's task tool.
The grinder is opinionated by design — it locks in defaults so the runner doesn't make new judgment calls every loop. The interesting decisions happen at planning and severity gating; everything else is mechanics.
Companion skill: cordy — how to drive cordy councils. The grinder
uses cordy idioms; don't re-explain them here.
Inputs the user must provide
- The work — described however you want. A file (
PHASES.md,ROADMAP.md,BACKLOG.md), an inline list in the trigger message, a "what's next" recommendation from a prior turn, a paragraph of prose, a single goal that decomposes naturally. The grinder reads what's given and serializes it into work units. - The repo — runs in CWD; works on whatever branch is current. Don't switch branches.
- Done criteria — what "complete" looks like. Without this the loop has no terminal condition that isn't "stuck".
Use what's already there. If the prior turn (or a referenced file) already articulates the work units — a 3-bucket recommendation, phases A through F, a numbered punch-list — serialize those units as-is. Don't re-decompose. The user did the planning; your job is to execute.
If the work is genuinely vague ("clean up the codebase"), ask once for specificity, then proceed.
Initial setup — TaskCreate the world
Before the first wake ping fires, do this in order:
1. Cordy preflight
cordy daemon start 2>&1 || true # idempotent
cordy --ephemeral drivers --json
Count drivers with probe.available: true. Fewer than 2 → halt with
STATUS.md. Note which drivers are up; that's the default panel.
2. Capture baseline
git rev-parse HEAD # baselineSha
<discovered test command> # baseline test counts
See "Tests pass = ?" below for runner discovery. Capture
{passed, failed, skipped} and the failure set as knownFailures
(those are pre-existing, not regressions).
3. TaskCreate the work-unit hierarchy
One parent task per work unit. Name it with the unit identifier as the user described it — "Phase B: resource pools become ruleset-driven", "Bucket 1: build cyberpunk ruleset", etc. Use the user's vocabulary (phase, bucket, item, milestone), not the skill's.
Sub-tasks per work unit, in execution order:
draft planplan councilrevise plan(only if council surfaces findings)implementtestdiff councilrevise impl(only if council surfaces findings)commit
Mark all as pending. Only the first sub-task of the first work unit will go in_progress immediately.
Example shape (matching what the user actually sees in the task panel):
◻ Phase B: resource pools become ruleset-driven
◻ B: draft plan
◻ B: plan council
◻ B: implement
◻ B: test
◻ B: diff council
◻ B: commit
◻ Phase C: combat formulas become hookable
◻ C: draft plan
...
4. Auxiliary state files (create lazily)
Some state can't fit in tasks — baseline info, accumulated changelog
entries, deferred findings, halt reports. These live under
<repo>/.cordyceps/grinder/:
<repo>/.cordyceps/grinder/
baseline.json — {baselineSha, baselineTests, panel,
droppedDrivers, consecutiveFailures}
changelog-pending.md — accumulated entries for end-of-run release
DEFERRED.md — deferred low-severity findings
STATUS.md — written on halt only
Don't create files preemptively — write them as the run produces
content. STATUS.md should be absent during a normal run.
5. Gitignore housekeeping
If <repo>/.gitignore exists and doesn't contain .cordyceps/, append
it. Don't create a .gitignore if absent (project may have
intentionally omitted it).
6. Mark first sub-task in_progress, begin
Now you're in the loop.
Wake-up recovery — every wake reads tasks first
TaskList()
Decision tree:
| Condition | Action |
|---|---|
| No tasks exist | Fresh run. Do initial setup. |
| Tasks exist, none in_progress, pending sub-tasks remain | Pick the next pending sub-task in order, mark in_progress, resume. |
| One sub-task in_progress | Resume from that exact step. Read auxiliary state (baseline.json, working tree diff vs baselineSha) to figure out where within the step you were. |
| All tasks completed | End-of-run release commit (Step 11), then halt. |
Crash recovery: working tree dirty AND in_progress sub-task doesn't
match the diff. Run git diff <baselineSha>; if the diff matches what
the recorded sub-task would produce, attempt to resume. If ambiguous,
write STATUS.md "ambiguous recovery — needs human" and halt.
The per-work-unit loop
┌─ for each work unit (parent task) ──────────────────────────┐
│ │
│ 1. baseline → SHA + test counts (per-unit) │
│ 2. draft plan → plan content; mark sub done │
│ 3. plan council → cordy council, background │
│ 4. revise plan (≤2) → integrate findings │
│ 5. implement → edit files; no scope creep │
│ 6. test → vs baseline; new failures→revert│
│ 7. diff council → cordy diff --staged, background │
│ 8. revise impl (≤2) → fix critical/sec/medium/quick │
│ 9. commit → conventional msg; no attribution│
│ 10. wake ping → background sleep+echo │
│ 11. mark unit complete → next work unit's tasks begin │
│ │
└─────────────────────────────────────────────────────────────┘
TaskUpdate to in_progress when starting each sub-task; completed
when finishing. Status changes are the breadcrumbs that let wake-up
recover.
Step 1 — capture per-unit baseline
Each work unit gets a fresh baseline (the previous unit's commit becomes
the new baseline). Update baseline.json:
git rev-parse HEAD # new baselineSha
<test command> # may differ from initial baseline
This gates regression detection. New failures vs. this unit's baseline = regression caused by this unit.
Step 2 — draft plan
Mark draft plan sub-task in_progress.
For substantive work units, the plan structure that produces good council critique:
- Goal — what this unit ships (one paragraph)
- Files in scope — specific paths the impl is allowed to touch
- Approach — high-level strategy
- Test plan — which tests will exercise the new code
- Acceptance — what makes the unit done
For trivial units (one-line fixes, doc-only changes), skip the template; a paragraph in the task description is enough.
Write to a tempfile when councilling:
cat > /tmp/grinder-${UNIT_ID}-plan.md <<'PLAN'
...
PLAN
Mark draft plan completed; mark plan council in_progress.
Step 3 — plan council
Council the plan via cordy. Use the cordy skill's host-as-chair pattern:
cordy council review /tmp/grinder-${UNIT_ID}-plan.md \
--panel codex,gemini,claude:opus \
--no-chair --timeout 600 --json \
> /tmp/grinder-${UNIT_ID}-plan-council.json 2>&1
Run with the Bash tool's run_in_background: true. The wake
notification fires when the council finishes — don't poll, don't
Monitor-stream (cordy buffers per-reviewer output until all reviewers
finish; there are no intermediate frames).
While the council runs in background, you can do useful prep
(read related files, draft scaffolding for the impl). When the
notification lands, read reviews[]. You're the chair (host-as-chair).
Mark plan council completed.
Step 4 — revise plan (only if needed)
Council findings are advisory. Rules:
| Severity | Action |
|---|---|
block-ship |
Address (incorporate) OR override with one-line technical rationale. No process attribution. |
critical, security |
Same as block-ship. |
medium, quick-win |
Address if cost is low; otherwise defer to DEFERRED.md. |
style, maintainability |
Your call. Override silently if you disagree. |
If no findings worth addressing → skip revising; mark revise plan as
completed (with a brief note) and proceed.
If findings warrant changes:
- Mark
revise planin_progress - Integrate the changes
- Optionally re-run the council if changes were substantive
- Cap at 2 council rounds total per gate. After round 2, decide and proceed.
- Mark
revise plancompleted.
If round 2 surfaces fundamental disagreement (a reviewer flags the unit as wrong-direction with substantive reasoning you can't refute) → write STATUS.md and halt. Don't bulldoze structural objections.
Step 5 — implement
Mark implement in_progress.
Edit only files named in the plan's "Files in scope" + their direct imports. "While I'm here" cleanup is forbidden — backlog it to DEFERRED.md.
If implementation reveals a need for files outside scope:
- Transitively required (a type used by an in-scope file) → allow.
- Separate concern → STOP. Update the plan with the scope change, decide whether to extend this unit (rare) or split it (default — add a new work unit, advance the current one to "blocked", continue).
Mark implement completed.
Step 6 — test
Mark test in_progress. Run the test command. Compare to baseline:
| Result | Action |
|---|---|
Same failed count, same failure set as baseline |
Pass — proceed. |
New failures (not in baselineTests.knownFailures) |
Regression. git restore <files>, retry implementation. |
| Test command itself fails to run | Halt with STATUS.md. Tests are a prerequisite. |
| 2 retries, regression persists | Mark unit blocked, increment consecutiveFailures, continue to next unit. |
For long test suites (5+ minutes): run with run_in_background: true,
do other prep, wake when tests finish.
Mark test completed.
Step 7 — diff council
Mark diff council in_progress. Stage exactly what's intended for
this commit:
git add <files-from-plan>
git status # confirm staged matches intent
git diff --staged --stat # double-check what reviewers will see
Then run the diff council on staged-only:
cordy council diff --staged \
--panel codex,gemini,claude:opus \
--no-chair --timeout 600 --json \
> /tmp/grinder-${UNIT_ID}-diff-council.json 2>&1
Background + wake-on-completion.
If unstaged changes exist post-implementation that weren't staged — cruft escaped scope — halt and surface. The grinder shouldn't silently leave or revert unexplained changes.
Mark diff council completed.
Step 8 — revise impl (only if needed)
Severity gate for the diff council:
| Severity | Action |
|---|---|
critical, security |
MUST address before commit. Edit, re-stage, re-run diff council (counts as a revision round). |
medium, quick-win |
Address — cost-benefit favorable. |
low, info |
Defer to DEFERRED.md. |
style |
Defer or accept silently. |
If nothing to revise → skip; mark sub-task completed, proceed.
After 2 revision rounds with critical/security findings still unaddressed → halt and surface.
Step 9 — commit
Mark commit in_progress. Per CLAUDE.md commit hygiene:
- Conventional Commits:
<type>(<scope>): <subject>under ~70 chars - Body explains the technical why — no process attribution (no "council found", no "C1/R2 noted", no "after review", no "spawned panel surfaced X")
- No AI attribution
- Stage with intent (already done in Step 7)
- After commit:
git show --stat HEADto verify only intended files landed. If extra files appeared →git reset HEAD~1, redo cleanly.
Append a one-line entry to changelog-pending.md:
- fix(council): pin reviewer mode at spawn (commit abc1234)
- feat(rulesets): add ruleset registry plugin (commit def5678)
Mark commit completed.
Step 10 — wake ping
Last action before "ending" the loop iteration:
sleep 1 && echo "lazarus"
…run as a Bash tool background task (run_in_background: true). The
completion notification wakes the next loop iteration.
This pattern survives auto-compact: the OS process is independent of the context window, the notification lands, the new context reads the task list and resumes.
If you launched a long test suite or council in background and it's still running when you'd otherwise fire a ping, don't fire a separate ping — the existing background task's completion is itself a wake signal.
Step 11 — mark work unit complete; advance
TaskUpdate the parent work-unit task to completed. The next work
unit's first sub-task (draft plan) becomes the next pending item;
the next wake will pick it up automatically.
If this was the last work unit, do the end-of-run release commit:
- Read
changelog-pending.md. - Bump version in the appropriate manifest:
package.json→versionCargo.toml→[package] versionpyproject.toml→[project] versionplugin.json+marketplace.json(Claude Code plugins) → both
- Patch bump unless work shipped breaking changes (pre-1.0: minor for any breaking change; post-1.0: major).
- Move pending entries into
CHANGELOG.mdunder a new## [X.Y.Z] — YYYY-MM-DDsection, ordered by Keep-a-Changelog category. - Atomic commit: version bump + CHANGELOG entry together.
- Delete
changelog-pending.md. - Write a final
STATUS.mdsummarizing the run (units done, deferred count, commits made, tests passing). - Stop the loop — no wake ping.
Do NOT push, tag, or release in the loop. Those are explicit human operations.
Decision protocols
Council = advisory; runner is final
The runner reads council output, decides. The council has no veto.
- Critical/security findings → must address; override only with substantive technical rationale in the commit body (no council attribution).
- Style/maintainability → free to override silently.
- Block-ship verdict from a unanimous council → stop, surface, ask user. Do not silently override unanimous structural objection. Final-decision-maker means judging findings, not ignoring them.
Tests pass = ?
Discover the test runner from the manifest, in this order:
| Manifest signal | Command |
|---|---|
package.json scripts.test |
npm test (or pnpm test / yarn test if lockfile indicates) |
Makefile with test target |
make test |
pyproject.toml (look for pytest, tox, nox config) |
the configured runner |
Cargo.toml |
cargo test |
go.mod |
go test ./... |
.github/workflows/*.yml test job |
mirror the CI command |
| None of the above | halt; ask user to specify |
Pass = exit 0 + no new failures vs baseline + lint/build clean (run the project's lint and build commands too if they exist; same discovery pattern).
Revision deadlock
Cap revision rounds at 2 per gate (plan council, diff council). After round 2:
- Outstanding findings are critical/security and the runner can't resolve → halt with STATUS.md.
- Otherwise → runner decides, proceeds.
Don't loop indefinitely chasing reviewer agreement; reviewers from different families won't fully converge on subjective findings.
Constraints (non-negotiable)
No process attribution anywhere
Commits, code comments, CHANGELOG, PR bodies. No "per council", no "C1/R2", no "after review", no "spawned panel found X", no "chair recommendation", no "round-2 convergence". Describe the technical what changed and why. Comments only when logic is non-obvious.
This restates a global rule because the grinder is especially prone to slipping into review-stage shorthand — every unit ends in a council, and the temptation to write "fixed per chair" is strong. Resist.
Destructive ops have a grinder-context auto-pass
Auto mode normally prompts for destructive operations. The grinder's loop cannot pause for prompts. Inside the per-unit loop, these are pre-authorized:
rmof git-tracked files (recoverable viagit restore)git reset --hard <baselineSha>to revert a failed implementation- Force-push to a non-main feature branch (in-progress branch cleanup)
- Schema migrations behind a feature flag
NOT auto-passed — still halt and write STATUS.md:
rm -rfof untracked files- Force-push to
main/master/ production branches - Anything writing outside the repo
- Changes to CI workflows, secrets, or release pipelines
- Dropping database tables, deleting users, sending external messages, publishing packages
Scope cap
Implementation may touch only files named in the plan's "Files in scope" + their direct imports. Cleanup outside scope → backlog to DEFERRED.md. Resist scope creep.
Tempfile hygiene
Clean up /tmp/grinder-* and /tmp/council-* at end of each work
unit. Over a 12-hour run these accumulate.
50-commit cap (CLAUDE.md)
Don't proactively manage. If hit, the grinder's git commit will
fail; squash later as a release-prep task. Not worth interrupting the
loop for.
Halt-and-report triggers
Write <repo>/.cordyceps/grinder/STATUS.md and stop the loop (no
wake ping) on:
| Trigger | What goes in STATUS.md |
|---|---|
| Destructive op outside the auto-pass list | What was attempted, why blocked |
| Plan-council unanimous block-ship after 2 rounds | Reviewer findings + runner's analysis |
| Diff-council critical/security findings unaddressable after 2 rounds | Same |
| Test infrastructure missing or unrecognized | What was tried + ask user to specify |
| 3 consecutive work-unit failures | Unit IDs + failure modes |
| Driver count drops below 2 mid-run | Which drivers dropped + why |
| Goal-impossible discovery during implementation | What was discovered + suggested next direction |
| Cordy daemon won't start after one restart attempt | Daemon error + suggested action |
| Ambiguous crash-recovery state | Diff vs baseline + what step was last recorded |
STATUS.md format:
# Grinder halted: <YYYY-MM-DD HH:MM>
## Trigger
<which trigger above>
## Context
<what work unit, what step, what was happening>
## What's complete
<commits made, units done>
## What's deferred (DEFERRED.md)
<count + categories>
## What's needed from you
<one specific question or decision>
Cordy preflight on every wake (not just startup)
cordy daemon start 2>&1 || true # idempotent
DRIVERS=$(cordy --ephemeral drivers --json)
AVAILABLE=$(echo "$DRIVERS" | jq -r '[.[] | select(.probe.available)] | length')
[ "$AVAILABLE" -lt 2 ] && halt
If a panel member errored or parse-failed in the previous unit, drop
it from the default panel for the rest of the run. Don't keep retrying
a broken driver. Track in baseline.json.droppedDrivers[] with the
reason.
If the daemon failed to start, retry once. Second failure → halt with STATUS.md.
Long-running councils — wake on completion, don't poll
Council reviews on real files take 5–15 minutes. Cordy buffers per-reviewer output until all reviewers finish, then writes one JSON blob in one shot.
- Run councils with Bash
run_in_background: true. - Do other useful work while waiting (preflight for the next step, read related files, draft the next unit's plan).
- Wake when the notification lands.
- Do not Monitor-stream — there are no intermediate events.
- Do not
tail/wc -cpoll — output is empty until the slowest reviewer returns. - Do not sleep-loop — the auto-notification is the signal.
If a council hasn't notified in ≳1.5× the panel's max --timeout,
something is genuinely stuck. Investigate, don't blindly retry.
DEFERRED.md format
# Deferred findings — generated by grinder
## Work unit: B (resource pools)
### style: Magic number 30000 in chunkByLines
Could be named MAX_CHUNK_BYTES (already defined elsewhere — minor
refactor).
### info: Comment in src/foo.ts:42 mentions deprecated API
Outdated reference; fix on next maintenance pass.
## Work unit: C (combat formulas)
...
The user reviews this at end of run and can spawn follow-up work or discard.
Anti-patterns
- Don't activate on single-task asks. The grinder is overhead for one feature.
- Don't re-decompose work the user already articulated. If they gave you "buckets 1, 2, 3" or "phases A through F", serialize those into tasks as-is. The user did the planning.
- Don't ask questions between work units. The roadmap is the contract; decide and proceed.
- Don't bulldoze unanimous structural council objections. The runner is final, but final means judging findings, not ignoring them.
- Don't commit unrelated drift. Stage with intent, verify with
git show --stat HEAD. - Don't write process attribution. No "council found X", no "per reviewer feedback". Technical why only.
- Don't bump the version on every commit. Version bump = end-of-run, single atomic change with CHANGELOG.
- Don't expand scope mid-unit to "fix something while I'm here". Backlog it.
- Don't poll background councils. Wake on notification.
- Don't keep retrying a peer that's been timing out / parse-failing for 2+ units. Drop it; proceed with the rest of the panel.
- Don't run if cordy isn't installed or fewer than 2 drivers are reachable. Single-driver "council" defeats the diversity benefit the whole skill is built around.
- Don't push, tag, or release from the loop. Those are explicit human operations.
- Don't fire multiple lazarus pings per loop iteration. One ping per "I'm done with this iteration" event. Multiple pings stack into thrash.
- Don't write a state.json with the work unit list. Tasks are the primary state. State files are for things tasks can't hold (baseline SHA, accumulated changelog entries, deferred findings, halt reports).
- Don't TaskCreate tasks before doing cordy preflight. If preflight fails (drivers missing, daemon broken), you'll have created tasks for a run that can't start. Halt cleanly first.