- name
- ponder
- description
- Convergent deliberation loop - repeated grading passes over one subject until the answer stops changing, with coverage enforced so no unit is silently skipped. Use when the user says "ponder <anything>" ("ponder the UI", "ponder our error handling"), "think this over properly", "keep thinking until it converges", "keep looping until it converges", "loop on this until it stops changing", "go through all the screens and grade them", "audit/rate/grade every X", or asks for any broad sweep-and-grade across many units where a single pass would miss things. Also use to resume or recover a ponder already in flight - "where were we", "carry on pondering", a lost thread mid-loop, or a .ponder/ directory in the project - where the move is to run ponder status rather than start over.
# Ponder
Ponder runs repeated passes over one subject until the answer stops changing: each pass revises the previous pass's findings - adding what was missed (`Dreamer` → `add`), sharpening what was vague (`Builder` → `edit`), re-grading what was mis-scored from the human user's lived and emotional perspective (`Empath` → `change`), and retracting what was unsupported or dead-code noise (`Skeptic` → `delete`). You decide how many passes to run; the engine measures whether stopping is honest and refuses to let you stop while a single enumerated unit is ungraded.
Unlike standard multi-pass prompts that force an LLM to manufacture nitpicks or hallucinate when asked to review again, Ponder treats **epistemic silence** as the goal of late passes: all four voices must verify the evidence and speak in `deliberation.json` (`voices = 4`), but a voice whose findings already hold emits **zero deltas**. Two consecutive fully reviewed, confident quiet passes earn `stop (converged)`.
> **Lost the thread mid-ponder? Run `ponder status`.** It names the pass you are in, the phase you owe, and the literal next command. If you do not know the slug - or do not know whether a session exists at all - run `ponder list` first; a `.ponder/` directory in the project means one probably does. Do not start over: a second session throws away the first one's ledger, which is the whole value. See [Resumption](#resumption) before you conclude anything is missing.
The binary is `ponder` on `PATH` (`~/.local/bin/ponder`), `${CLAUDE_PLUGIN_ROOT}/bin/ponder` (when installed as a plugin), or `~/code/ponder/bin/ponder`. If `command -v ponder` fails and neither path exists, clone [`https://github.com/Quinnod345/ponder`](https://github.com/Quinnod345/ponder) to `~/code/ponder` and run `~/code/ponder/install.sh` from that reviewed checkout. **Every command ends with a `NEXT:` line naming the phase you now owe.** That line is the engine driving you, and it is the thing that survives a compaction.
Every `--file` in this document is a path you write first. Make the scratch directories once, before the first command: `mkdir -p .ponder/tmp/batch`. The engine ignores directories under `.ponder/` that hold no `session.json`, so `.ponder/tmp/` is a safe place to stage input - `tmp/` for what you hand the engine, `tmp/batch/` for what fan-out agents write.
The terminal transcripts below are abbreviated and are there to show you the *shape* of a pass. Where a transcript and the engine disagree, the engine is right - read its actual output.
Deeper references, load only when you need them:
- `references/protocol.md` - every command, every flag, every JSON shape, the verdict rules, exit codes, and how reconciliation and `verify --repair` work.
- `references/perspectives.md` - Skeptic / Builder / Dreamer / Empath as runnable prompts, and how to fan them out.
- `references/harness-notes.md` - what differs between Claude Code and Codex: subagents, sandboxing, psql, install. Also the session lock, in full, if you are about to fan work out.
Read the lens pack for your subject (`lenses/<lens>.md` in the ponder repo) before pass 1. It defines what a unit is, how to enumerate exhaustively, the dimensions, and what each rung of the 1-5 scale means.
## The loop
### Once, at the start
**First check that you are actually at the start.** If a pondering of this subject is already open, `start` is the wrong command and a second session throws away the first one's ledger.
```sh
ponder list # is one already open? if yes, go to Resumption
ponder doctor # environment + whether the OCA bridge is reachable
ponder start "the UI" --lens ui --max-passes 6 --min-passes 2
```
`start` prints **Prior pondering** if the OCA remembers thinking about this subject before - read it, it is where you left off last time. The OCA is optional; an unreachable one degrades silently and never blocks a pass.
### Every pass, in this order
1. **predict** - before you look at anything.
`ponder predict --churn 0.35 --claim "Pass 1 over-graded empty states and never opened the error paths."`
You are stating what this pass will do to the finding set. The engine scores it as `surprise = |predicted - actual|`. Guessing low every time to look calibrated corrupts the only signal that tells you whether you understand the subject yet.
**`--churn` is validated to 0-1 - anything outside that is refused, not clamped: `ponder predict --churn 1.2` exits `1` and records nothing.** `0` and `1.0` are both legal. On pass 1 the number is recorded but not scored - there is no previous pass, so the engine reports no churn to take the distance from. Write the honest guess and put the real content in `--claim`. The signal starts at pass 2.
2. **survey** *(pass 1 only)* - enumerate every unit exhaustively, using the lens pack's enumeration recipe, or let the CLI walk disk/git directly with `--scan <dir>`.
`ponder units --file .ponder/tmp/units.json --scan src/routes --strict-scope` (or `ponder units --scan src/routes` to auto-seed every file in `<dir>`)
When `--scan <dir> --strict-scope` is passed alongside `--file`, the engine cross-checks every `unit.path` against the actual files on disk under `<dir>` and exits `2` if any file was omitted. Later passes amend the inventory with deltas instead: `unit-add` for what the survey missed, `unit-edit` for what it got wrong, `unit-delete` for what is out of scope.
3. **appraise** - grade every unit against the lens rubric.
Pass 1: `ponder grade --file .ponder/tmp/findings.json`
Pass 2+: re-read the units and the current findings. The output is deltas, filed at step 5 - on later passes you appraise *by* revising, so there is no separate command.
4. **deliberate** - put the current finding set to the four perspectives (`references/perspectives.md`).
`ponder deliberate --file .ponder/tmp/deliberation-2.json`
**This is where the through-line gets written, not just where deltas get produced.** The findings are per-unit by construction; the argument is the only place in the loop where anything is said about the set as a whole. On the 228-screen run the single most valuable output was not one of the 760 findings - it was the Dreamer's cross-screen claim, which no individual grade could have carried. Write each `argument` as a paragraph that would be worth reading on its own, name the pattern across units, and let the deltas fall out of it. An argument written as a list of delta stubs has already thrown away the part that mattered.
Skipping is legal and recorded against you: `ponder deliberate --skip --why "..."` marks the pass skipped in `passes/NN/deliberation.json`, surfaces the reason in `report.md`, and - when the OCA is reachable - raises a `deliberation_skipped` metacognitive observation. The record survives with the OCA off; only the observation does not.
5. **revise** - apply the deltas the deliberation produced.
`ponder delta --file .ponder/tmp/deltas-2.jsonl`
Every delta carries `op`, `perspective`, and a non-empty `why`. The engine rejects a delta without a `why`. On pass 2+ this call satisfies appraise as well.
**Token-efficient internal notation:** To keep multi-pass token usage low (~56% reduction across 4 passes), you may write internal machine-to-machine `deliberation.json` arguments and `deltas.jsonl` `why` strings using compact causal arrows (`"safe-query.js:5 binds $1 -> SQLi claim false -> retract"`), while keeping all user-facing finding `title`, `detail`, and `recommendation` fields in clear, complete prose.
6. **judge** - `ponder judge` computes the metrics and prints advice.
7. **decide** - `ponder decide <continue|stop|escalate> --why "..."` commits your call.
On pass 2+ the engine skips survey and collapses appraise into the `ponder delta` call, so the command sequence is **predict → deliberate → delta → judge → decide**. Phase order is enforced: a command that arrives out of order exits `2` and names the phase you owe. Believe that message over your memory. And if at any point in this list you are not certain which pass you are in or what you already did, stop guessing and run **`ponder status`** - it is cheap, it takes no lock, and it answers both ([Resumption](#resumption)).
`voices` counts the perspectives that **spoke in the deliberation** - a non-empty `argument` in `passes/NN/deliberation.json`. A Skeptic who opened the evidence, found the finding sound and said so has voted; finding nothing is a result, and a voice should not have to manufacture a delta to be counted. A pass that skipped deliberation heard nobody and reports `voices` 0. Watch the delta *ops* separately: a pass whose deltas are all `add` accumulated rather than argued, whatever its `voices` says.
## How many passes
`ponder judge` **advises**. `ponder decide` **commits**. The advice is computed from coverage, novelty, churn, thrash, surprise, stall, confidence floor and voices (formulas and the ordered verdict rules are in `references/protocol.md`). It cannot see what you know, so it can be overridden - with one exception below.
**Write the `--why` before you choose.** Whichever way you are leaning, draft the sentence you would put in `ponder decide` and read it as the user will read it in `report.md` in a month. "Two quiet passes, full coverage, every open must above 0.5 confidence" is a stop. "Nothing new came up" is not - it is the sentence you write when you are tired. If you cannot write a `--why` that survives being read back, you have your answer: run another pass.
**Default to the advice.** You may override it in either direction, and both directions cost the same thing: a `--why` of at least 12 characters, written permanently into `report.md` under **Overrides**. Overriding a `stop` into another pass is the cheap mistake - one more pass. Overriding a `continue` into a `stop` is the expensive one, and the bar for it is the whole honest-stop checklist below, every item, not a feeling that you are done. The two checklists below are where the decision actually gets made - the engine's rules got you a default, and you are the only one in the loop who can see whether it is right.
Override toward another pass when any of these is true:
- **The last pass changed your mind about something important.** If pass N killed a finding you would have shipped, moved a severity, or opened a dimension nobody had looked at, then pass N+1 has somewhere real to go. Late novelty is not noise - it is evidence the subject is deeper than your inventory.
- **A whole class of unit was graded shallowly.** One finding on a screen no earlier pass had seen is a first look, not a grade. Same for "all six modals got one line each". Coverage counts units, not depth; it cannot see this and you can.
- **The deliberation barely happened.** `voices < 3` - fewer than three perspectives argued at all - or four spoke and the deltas still came back all one op. Run another pass and put the Skeptic and the Empath on it specifically; the missing voices are usually the ones that would have cut findings, not added them.
- **Your prediction was badly wrong toward more churn** (`surprise > 0.3` with actual above predicted). You do not model the subject yet, so you cannot claim it has settled.
**Stopping is honest when all of these hold:**
- `coverage` is 1.0 (the engine enforces this; see below).
- The last two passes produced no `add` you would defend as new ground.
- `churn` is near zero, and what did move was ranking, not substance.
- Every open `must` finding has `confidence >= 0.5` - that is `confidenceFloor 0.00` on the judge table, not merely under the 0.2 the engine enforces. You are about to recommend work; a `must` you cannot defend is a guess you are asking someone to act on. If the floor is not zero, either go and get the evidence (a `change` raising the confidence) or drop the severity.
- The last pass you would call substantive heard at least three voices. `ponder decide stop` warns below three and lets you through; if you go through, say why in the `--why`.
- Every unit you dropped from the inventory, you dropped for a reason you would say out loud to the person who asked for the pondering. See the phantom rule below.
**Stopping is not honest** because you are bored, because the context got long (that is what `ponder status` and a fresh subagent are for), because the remaining findings are tedious to write up, or because you hit `maxPasses` while the numbers were still moving. That last one is a **budget stop** - the engine reports it as `budget`, never as convergence, and so should you.
**A converging pass is supposed to feel quiet, and a pass that emits nothing is a real result.** The last pass of the 228-screen run produced zero deltas from all four voices - every perspective read the set, found nothing it would change, and said so - and that was the correct, honest outcome, not a wasted pass. It is the strongest evidence of convergence the loop can produce: four voices looking for work and finding none. Expect the pass count to feel anticlimactic near the end, and do not manufacture a delta to prove you were there. The mechanics allow it: `ponder delta` takes an empty file, applies nothing, prints `0 deltas applied` and `delta voices 0/4 - no perspective emitted a delta`, and advances the phase like any other pass. A `voices 4/4` with `delta voices 0/4` and a deliberation whose arguments each say concretely *what was re-checked and why it held* is a converged pass. The same row with four arguments saying "looks fine" is three voices going through the motions, and only you can tell which one you just ran.
**`blocked` cannot be overridden.** When `coverage < 1.0` the verdict is `blocked`, it names every ungraded unit, and `ponder decide stop` exits non-zero. Every other verdict is a judgement call about diminishing returns; this one is a fact about work not done, and it is the exact failure ponder exists to prevent - a sweep that grades 11 of 12 screens and reports as though it graded 12. There is exactly one move that always works: **go and grade the missing units** (`ponder delta` with `add`s). There is a second move, narrow and checked by the engine, for a unit that does not exist at all - the phantom rule below.
Do not go looking for a way around it. `ponder decide` recomputes coverage from `units.json` and `findings.json` at the moment you decide, so a stale `metrics.json`, a hand-edited `verdict.json`, a lowered `--min-passes`, `--json` mode and a second session all leave the gate exactly where it was. Verified: every one of those still exits `2`, `--json` included - it just returns the refusal as `{"ok": false, "code": 2}` instead of printing it.
**`escalate` means the loop is oscillating** (`thrash > 0.25` - passes reversing each other on the same targets). Another identical pass will not help. Rotate which perspectives lead, widen the evidence you are reading, or split the subject into two sessions and ponder each. Commit it with `ponder decide escalate --why "..."` and then actually do something different.
Budget shape: `--min-passes 2` because a single pass is an opinion, not a pondering; `--max-passes 6` for a broad sweep, 3-4 for a narrow subject.
### Correcting a unit - the caption has to follow the finding
**A unit's `label` is a caption on the inventory, and a pass that corrects the finding underneath it has to correct the caption too.** Pass 1 enumerates a screen as `"Marketing footer - both links 404"`. Pass 3 opens the file, finds `/privacy-policy` resolves fine, and rewrites the finding to say so. If the caption cannot move, `report.md` now leads with a headline that contradicts the finding printed directly beneath it. The real 228-screen run shipped exactly that, because there was no way to fix it. There is now.
```
{"op":"unit-edit","target":"screen.marketing.footer","perspective":"builder","why":"Pass 1's label said both links 404; the finding under it now says the opposite, and says it correctly.","set":{"label":"Marketing footer - /privacy-policy resolves","meta":{"route":"/"}}}
```
- **It sets `label`, `path`, `kind`, `meta` - and nothing else.** Anything else is refused by name.
- **It can never set `id`.** The id is the join key between `units.json` and `findings.json`; moving it would orphan every finding on that unit. The engine refuses and says so. If the id itself is wrong, `unit-delete` and `unit-add` the corrected unit, and pay what that costs coverage.
- **`meta` merges key by key.** Correcting a route leaves the `state` marker sitting beside it alone. Every other field replaces.
- **Coverage does not move.** That is the point. Correcting a caption by deleting the unit and re-adding it would drop coverage below 1.0 and block the stop - a wrong caption would reopen the session. `unit-edit` costs `churn` instead, which is honest: something did move.
- **It is a revision, so it thrashes like one.** A label changed in pass 2 and put back in pass 3 registers as a reversal.
- **It is on the record.** Every `unit-edit` appears in `report.md` under **Corrected screens** - or whatever your lens calls a unit - with its pass, its voice, the new values and your `why`. And a label edited by hand in `units.json` with no ledger record behind it is a divergence: `judge`, `decide`, `report` and `apply` all refuse the session until it reconciles. Correcting a caption on the record is one delta. Correcting one off the record is not available.
Use it when the pass genuinely learned something about the unit. Do not use it to tidy wording you simply prefer - that is churn you will have to defend at `decide`.
### The phantom rule - deleting a unit you never looked at
`unit-delete` is the only op that shortens the ungraded list, so it is the one the engine watches hardest. What it costs coverage depends on whether you ever looked:
- **The unit carries a finding** (even a retracted one - withdrawing a judgment does not un-see the thing). Deleting it is an honest scope change. It leaves the inventory and the coverage denominator.
- **The unit was never graded.** It leaves the inventory and **stays in the coverage denominator**, still counted, still ungraded, gate still shut. Nothing errors. Coverage simply does not move, and if you were deleting it to make the number go up, it did not work.
- **The unit was never graded and the delta carries `"phantom": true`.** Now the denominator drops - after the engine has checked you. It resolves **every path the unit has ever carried** from the project root: **any of them exists → the whole batch is rejected** (`Something you can open is not something pass 1 imagined`); all of them missing and the path never moved → accepted and recorded for ever as `verified-missing`; the path was edited, or there was never one → accepted and recorded for ever as `unverified`. Every accepted phantom is named permanently in `report.md` under **Phantom units**, with your `why` quoted underneath it.
```
{"op":"unit-delete","target":"screen.legacy.print","perspective":"skeptic","why":"Opened src/routes/legacy/print.tsx - the file is gone; the route was removed in #1841 and pass 1 enumerated it off a stale router snapshot.","phantom":true}
```
**Now the part that matters.** `phantom` is a claim that a route does not exist - that pass 1's survey enumerated something the software does not have. It is not a claim that you ran out of context, or time, or patience.
**If you did not look at a screen, do not delete it from the list. Go look at it.** That is the whole job. The engine can catch you only when the unit has a path to check; a unit with no path is accepted on your word alone, and the record will say `unverified` next to your `why` for as long as the session exists. Writing `"phantom": true` about a route you simply never opened is lying to a file that keeps it - and it is precisely the half-delivered sweep ponder was built to make impossible, done by hand.
Before you write `phantom`, answer one question: **what did I open, and what was not there?** If the answer names a path and an absence, put it in the `why` and file the delta. If the answer is "nothing, I just didn't get to it", you have a unit to grade, not a phantom to declare.
**And the path is not a way around that.** `unit-edit` can move a unit's `path`. Two deltas - point the unit at a filename that does not exist, then claim it never existed - would have the engine certify the absence of a screen that is sitting right there in the repo. That is the same dishonesty as the last paragraph wearing a new op, and the engine closes it: **a phantom claim is checked against every path the unit has ever carried** - the one its `unit-seed` or `unit-add` gave it, every value any `unit-edit` has set since, the one `units.json` holds now, and any set earlier in the same batch. One survivor sinks the whole batch, and the refusal names the path it found and the pass that set it:
```
delta line 3: unit "screen.auth.sso" is claimed phantom, but a path it has carried exists:
/abs/src/routes/auth/sso.tsx (carried by the unit-add that created it in pass 4). Something you
can open is not something pass 1 imagined. A phantom claim is checked against every path the unit
has ever had, so editing the path first does not clear it.
```
Editing a path never *strengthens* a phantom claim either. If the path moved and no version of it exists, the claim is accepted but permanently recorded `unverified` - never `verified-missing` - and `report.md` says which paths were checked. Every version was missing, and the engine still will not certify it, because "missing" and "never existed" are only the same claim when the target stayed still. So a moved path buys you exactly nothing and leaves a permanent note that the unit moved before it vanished.
If the path in the inventory is genuinely wrong, correct it with a `unit-edit` and **go grade the screen**. That is what the op is for. It is not a step on the way to deleting the screen.
**And one refusal that is not about you at all.** Every path is resolved from the session's project root, so the check is only as good as that root. If the root was *guessed* - no `--project`, and no `.git` / `package.json` / other project marker anywhere above the directory you started in, so the engine adopted a stray `.ponder` or fell back to the working directory - then a path that fails to resolve tells you nothing, and the claim is refused:
```
delta line 2: unit "screen.settings.profile" is claimed phantom and none of the 2 paths it has
carried (src/routes/settings/profile.tsx, src/routes/settings/profile-v2.tsx) resolves under
/private/tmp - but that project root was guessed, not established (no project marker anywhere
above the working directory; adopted an ancestor .ponder). A file that is missing from the wrong
directory is not a screen that never existed.
```
This is the engine refusing to certify its own guess. `ponder start` and `ponder status` print the project root and what decided it, so check that line first: if it says the root was adopted or fallen back to, and it is not where the software actually lives, start the session again with `--project <path>` and the claim will be checked properly. A unit that never had a path is unaffected - there was nothing for a root to resolve, and it is still accepted as `unverified` on your word.
The honest exits when you genuinely cannot finish are `ponder decide continue` (do the work next pass) and `ponder abort --why "..."` (say what stopped you). Both leave a record that reads true later. A false phantom does not.
## Fanning out over many units
A sweep of 40 screens does not fit in one context and should not try to. Batch the *grading*; keep the *session* single-threaded.
**Batching rule: 8-12 units per agent.** Fewer wastes a context; more and the last units in the batch get a thinner read than the first - exactly the shallow-grade failure you would then have to spend a pass fixing.
Non-negotiables for fan-out:
View on GitHub