# Grinder

> Autonomous looped repo work. For each work unit (phase, bucket, item, whatever the user calls them): plan → peer-review council → revise (≤2 rounds) → implement → test → diff council → commit. Background-ping wake-up between iterations. Repeat until the user's roadmap is exhausted or a halt trigger fires. PERSISTENCE: Claude Code's task tool is the primary state — work units and per-unit grinder steps are TaskCreate'd at run start, TaskUpdate'd on every transition. Tasks survive auto-compact, so wake-up reads the task list to find where it left off. ACTIVATE on: "grind the backlog", "grind through this", "auto-grind <X>", "run the grinder on <X>", "go autonomous on <X>", "continue with the <buckets|phases|items>", "use the auto repo grinder". REQUIRES: `cordy` on PATH and at least 2 peer drivers reachable. Refuses to start with fewer drivers — single-peer "council" defeats the diversity benefit. Auto mode (or equivalent permission-bypass) must be active; the loop cannot pause for permission prompts. NOT for o

- Skill: `ellyseum/grinder` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ellyseum/grinder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ellyseum/grinder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: ellyseum (https://skillmd.com/u/ellyseum)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ellyseum/grinder

---


# 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

1. **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.
2. **The repo** — runs in CWD; works on whatever branch is current.
   Don't switch branches.
3. **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
```bash
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
```bash
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 plan`
- `plan council`
- `revise plan` (only if council surfaces findings)
- `implement`
- `test`
- `diff council`
- `revise 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`:
```bash
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:
```bash
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:

```bash
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 plan` in_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 plan` completed.

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:
```bash
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:
```bash
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 HEAD` to 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:
```bash
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:

1. Read `changelog-pending.md`.
2. Bump version in the appropriate manifest:
   - `package.json` → `version`
   - `Cargo.toml` → `[package] version`
   - `pyproject.toml` → `[project] version`
   - `plugin.json` + `marketplace.json` (Claude Code plugins) → both
3. Patch bump unless work shipped breaking changes (pre-1.0: minor for
   any breaking change; post-1.0: major).
4. Move pending entries into `CHANGELOG.md` under a new
   `## [X.Y.Z] — YYYY-MM-DD` section, ordered by Keep-a-Changelog
   category.
5. Atomic commit: version bump + CHANGELOG entry together.
6. Delete `changelog-pending.md`.
7. Write a final `STATUS.md` summarizing the run (units done, deferred
   count, commits made, tests passing).
8. 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**:
- `rm` of git-tracked files (recoverable via `git 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 -rf` of 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:
```markdown
# 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)

```bash
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 -c` poll — 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

```markdown
# 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.

