diff --git a/docs/README.skills.md b/docs/README.skills.md
index 0c7643e3c..73d6cdba5 100644
--- a/docs/README.skills.md
+++ b/docs/README.skills.md
@@ -335,6 +335,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [playwright-explore-website](../skills/playwright-explore-website/SKILL.md)
`gh skills install github/awesome-copilot playwright-explore-website` | Website exploration for testing using Playwright MCP | None |
| [playwright-generate-test](../skills/playwright-generate-test/SKILL.md)
`gh skills install github/awesome-copilot playwright-generate-test` | Generate a Playwright test based on a scenario using Playwright MCP | None |
| [poka-yoke](../skills/poka-yoke/SKILL.md)
`gh skills install github/awesome-copilot poka-yoke` | Mistake-proof code so misuse cannot be expressed, rather than warning against it. Use when designing an interface, schema, or state machine and the user wants it hard to get wrong ("make invalid states unrepresentable", "so callers cannot screw it up", "type-safe API", "pit of success"); when auditing existing code for footguns ("what could bite us here", "what is easy to misuse", "poka-yoke this repo", "review this diff for ways to get it wrong"); or when a bug has recurred and the fix must close the class rather than the case ("make sure this never happens again", "this is the third time"). Especially for money, auth, permissions, deletion, migrations, and pipelines where failure is silent. Classifies every finding by what happens when the mistake occurs and how the device notices, which is what keeps it from collapsing into generic code review. | `references/hazard-catalog.md`
`references/lang-python.md`
`references/lang-rust-go.md`
`references/lang-typescript.md`
`scripts/detect_hazards.py` |
+| [ponder](../skills/ponder/SKILL.md)
`gh skills install github/awesome-copilot ponder` | 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 " ("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. | `references/harness-notes.md`
`references/perspectives.md`
`references/protocol.md` |
| [postgresql-code-review](../skills/postgresql-code-review/SKILL.md)
`gh skills install github/awesome-copilot postgresql-code-review` | PostgreSQL-specific code review assistant focusing on PostgreSQL best practices, anti-patterns, and unique quality standards. Covers JSONB operations, array usage, custom types, schema design, function optimization, and PostgreSQL-exclusive security features like Row Level Security (RLS). | None |
| [postgresql-optimization](../skills/postgresql-optimization/SKILL.md)
`gh skills install github/awesome-copilot postgresql-optimization` | PostgreSQL-specific development assistant focusing on unique PostgreSQL features, advanced data types, and PostgreSQL-exclusive capabilities. Covers JSONB operations, array types, custom types, range/geometric types, full-text search, window functions, and PostgreSQL extensions ecosystem. | None |
| [power-apps-code-app-scaffold](../skills/power-apps-code-app-scaffold/SKILL.md)
`gh skills install github/awesome-copilot power-apps-code-app-scaffold` | Scaffold a complete Power Apps Code App project with PAC CLI setup, SDK integration, and connector configuration | None |
diff --git a/skills/ponder/SKILL.md b/skills/ponder/SKILL.md
new file mode 100644
index 000000000..0b4c3aab4
--- /dev/null
+++ b/skills/ponder/SKILL.md
@@ -0,0 +1,459 @@
+---
+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 " ("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/.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 `.
+ `ponder units --file .ponder/tmp/units.json --scan src/routes --strict-scope` (or `ponder units --scan src/routes` to auto-seed every file in ``)
+ When `--scan --strict-scope` is passed alongside `--file`, the engine cross-checks every `unit.path` against the actual files on disk under `` 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 --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 ` 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:
+
+- **Only the parent runs `ponder`.** Batch agents grade and return text. The engine does hold an exclusive lock on the session, so a race cannot corrupt it - but a race has a **loser**, and the loser's work is refused, not merged. Two concurrent `ponder delta` calls: one applies its batch and advances the phase; the other waits for the lock, reloads the session, finds it now owes `judge`, and exits `2` with nothing applied. A subagent that does not check its exit code will report success while its entire batch went nowhere. Collect JSONL from the batch agents, concatenate, and make one call yourself. (`references/harness-notes.md` has the lock's full behaviour - the wait, the stale reclaim, what the loser sees.)
+- **Give each agent the rubric, its own units, and the output shape** - the lens pack text, its unit list with paths, the dimensions, the 1-5 scale. Do not give it the whole finding set on pass 1; you want independent grades, not consensus.
+- **On revision passes, hand each agent only the current findings for its own units**, and ask for *deltas*, not a re-grade. Ask for a re-grade and you get thirty `add`s that duplicate what is already there, which the Skeptic then has to kill, which the thrash detector then flags.
+- **Ask for JSONL, one object per line, and nothing else** - no prose, no fences. A truncated line is droppable; a truncated JSON array is garbage. Concatenation is then `cat`.
+
+**In Claude Code:** use the Agent tool, one agent per batch, all launched in a single message so they run in parallel. Each agent writes its lines to `.ponder/tmp/batch/findings-.jsonl` and returns only the path plus a count. For a very large sweep (60+ units, or one where batches need retrying), the Workflow tool handles batching and result collection better than hand-rolled parallel agents.
+
+**In Codex:** there is no in-process subagent. Either spawn `codex exec` subprocesses, one per batch -
+
+```sh
+codex exec -C "$PWD" --sandbox workspace-write --skip-git-repo-check \
+ -o .ponder/tmp/batch/batch-3.txt \
+ "Grade these 10 screens against ~/code/ponder/lenses/ui.md. Units: ... .
+ Write one JSON finding object per line to .ponder/tmp/batch/findings-3.jsonl. Output nothing else." \
+ .ponder/tmp/all.jsonl
+node -e 'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d).on("end",()=>{const findings=s.split("\n").filter(Boolean).map((l)=>JSON.parse(l));process.stdout.write(JSON.stringify({version:1,findings},null,2)+"\n")})' \
+ < .ponder/tmp/all.jsonl > .ponder/tmp/findings.json
+ponder grade --file .ponder/tmp/findings.json
+```
+
+`setEncoding("utf8")` is not decoration. Without it the chunks arrive as buffers and each one is stringified on its own, so a multi-byte character that straddles a 64 KB read boundary is coerced to replacement characters. Reproduced deterministically: 65,535 ASCII bytes followed by `数` turns that one character into three `U+FFFD` without it, and survives intact with it. Whether it bites at all depends on where your curly quotes and accents happen to fall, which is the worst property a corruption can have - it ships a mangled title with a clean exit code and no warning, and the run that finds it is not the run that has the bug.
+
+**Stage batch files in their own directory (`.ponder/tmp/batch/`) and never let the concat glob match its own output.** `cat .ponder/tmp/deltas-*.jsonl > .ponder/tmp/deltas-2.jsonl` works exactly once: on the re-run you are told to do when a batch dies, the glob now matches `deltas-2.jsonl`, the shell truncates it before `cat` opens it, and `cat` ends up reading the file it is writing.
+
+What happens next depends on sort order, which is the part that makes this hard to believe until it bills you. Measured: with `deltas-1.jsonl` sorting *before* the target, `cat` copies batch 1 in, then opens `deltas-2.jsonl` and finds that same content waiting for it - a loop that fills the disk (capped at 20 MB with `ulimit -f` in the test, still climbing when it hit the cap). With the target sorting first, there is no loop at all: it reads an empty file and you get a quietly incomplete batch instead. So the failure is either a full disk or silent data loss depending on what you named the files, and "I tried it once and it was fine" proves nothing. Deltas need no wrapping, so:
+
+```sh
+cat .ponder/tmp/batch/deltas-2-*.jsonl > .ponder/tmp/deltas-2.jsonl # pattern cannot match the target
+ponder delta --file .ponder/tmp/deltas-2.jsonl
+```
+
+Deliberation does not fan out by unit. It fans out by perspective - four agents, one per voice, same finding set, each emitting only its own delta op. See `references/perspectives.md`.
+
+## Resumption
+
+**If you have lost context, do not guess and do not start over. Run `ponder status`.**
+
+```sh
+ponder status
+```
+
+```
+ponder status - ui-2026-08-24
+ session ui-2026-08-24 open
+ subject the UI
+ lens ui - unit = screen, 12 dimensions
+ grade on information-hierarchy, empty-states, loading-states, error-states, motion,
+ affordance-clarity, density, responsive-behaviour, accessibility,
+ copy-tone, consistency, delight
+ pass 3 of 6 (min 2)
+ owes deliberate
+ coverage 0.92 - 11 of 12 screens graded
+ findings 29 total · 26 open · 0 resolved · 0 deferred · 3 retracted
+ prediction churn 0.12 - "The invite route is small; one or two findings there and no…"
+ signals p2 novelty 0.12 churn 0.42 thrash 0.00 surprise 0.02 stall 0 conf-floor 0.11 voices 4/4
+ last verdict pass 2: blocked → decided continue - "Dreamer found /invite/:token. Cannot stop with a screen…"
+ oca reachable - postgres://localhost/oneiro
+ store /abs/path/.ponder/ui-2026-08-24
+
+ 1 ungraded screen - stop is refused until this list is empty:
+ screen.invite.accept
+
+ History
+ pass advice decision reason
+ 1 continue continue Pass 1 is the inventory plus a first opinion. Nothi…
+ 2 blocked continue Dreamer found /invite/:token. Cannot stop with a sc…
+
+NEXT: deliberate - ponder deliberate --file deliberation.json --session ui-2026-08-24 # or: ponder deliberate --skip --why ""
+```
+
+**Act on the `owes` row and the `NEXT:` line.** `NEXT:` is the literal command, already carrying `--session`; paste it. Everything else you need to resume is in the same block: which pass, what is still ungraded, what the last pass decided and why, and where the files are. The `signals` row is labelled with the pass it came from - `signals p2` while you are working pass 3, because the current pass has not been judged yet.
+
+### Getting back to work in five commands
+
+Do these in order. Do not substitute recall for any of them.
+
+```sh
+ponder status # 1. the pass, the phase you owe, the ungraded list
+ponder verify # 2. do the files still agree with the ledger?
+cat .ponder//findings.json # 3. the current truth - every live finding, with ids
+cat .ponder//ledger.jsonl # 4. how it got there: every delta, every why, in order
+ponder report # 5. optional, when you want it readable rather than raw
+```
+
+Then run whatever `NEXT:` printed.
+
+**`ponder verify` is the command for a session you did not personally leave in a known state** - one you inherited, one another agent was driving, or one whose last command was interrupted. It replays `ledger.jsonl` from line 1, derives what `units.json` and `findings.json` must contain, and compares - then compares the ledger against the per-pass `deltas.jsonl` files, which are the same records written a second time. Exit `0` and a summary means the session is sound. Exit `2` names every id and field that disagrees, and `judge`, `decide`, `report` and `apply` will refuse the session on the same grounds, so it is better to find out now than at the verdict.
+
+Every command writes `ledger.jsonl` **before** it writes the state files, so the only thing an interrupted command can leave behind is a ledger that is ahead of its own view - and that is repairable. **`ponder verify --repair`** replays the ledger, rewrites the state files to what those records must have produced, appends the missing tail to any pass's `deltas.jsonl`, and prints line by line what it rebuilt. It refuses, exit `2`, when a state file holds a unit or a finding **no ledger record ever created**: that data exists in exactly one place, so nothing can rebuild it, and it is either a forgery or an unrecoverable loss. Do not go looking for a flag past that refusal - there isn't one, and `--repair` never invents state. Without `--repair`, `verify` writes nothing at all, so running it is free.
+
+Steps 3 and 4 are not optional reading for a mid-loop pass. You cannot deliberate over a finding set you have not re-read, and you cannot avoid re-adding what the Skeptic killed two passes ago without the ledger in front of you - that is what `thrash` measures and what an `escalate` you caused yourself looks like. For a big set, read the findings for the units this pass is about, plus every ledger line touching them.
+
+Everything else you might need:
+
+- `ponder status --json` for the same thing machine-readable, including the ungraded unit list.
+- `ponder list` shows every session it can see from here - both stores, this project's `.ponder/` and the `~/.ponder/` fallback - with each one's status, pass, phase and coverage. It is how you find a slug you have forgotten. Every other command then takes `--session ` to target one. **Never start a new session because you lost the thread** - you would throw away the ledger, which is the whole value.
+- Nothing in `findings.json` or `ledger.jsonl` is ever erased. A retraction is a state, so pass 2's reasoning is still legible in pass 6 and a later pass can reverse an earlier pass's kill.
+- `ponder doctor` reconciles every session it can see and names any that fail under **Problems**, with the `ponder verify --session ` that diagnoses each one. That is how a machine full of sessions tells you which one is damaged.
+- **Never hand-edit `units.json` or `findings.json` to fix something.** The ledger is the authority and every decision-point command recomputes against it, so a hand-edit does not correct the session - it breaks it, and you will meet the refusal at `judge`. Everything you might want to fix that way has a delta: `edit`, `change`, `delete`, `unit-add`, `unit-edit`, `unit-delete`.
+- Re-running a phase you already satisfied exits `2` and names the phase you actually owe. That is not an error to work around; it is the state machine correcting your memory.
+- If the subject genuinely changed under you, `ponder abort --why "..."` and start a clean session. Silently re-scoping an open session poisons its metrics.
+
+If you were the parent of fan-out agents and one died, re-run that batch only. The state lives in files, not in anybody's context.
+
+**A compaction is not a reason to stop.** It is the failure mode the whole design is built around: the files hold the pondering, `ponder status` hands it back, and the loop continues from exactly where it was. Deciding `stop` because your context got long is the dishonest stop wearing a technical excuse - and if coverage is short, the engine will refuse it anyway.
+
+## Worked example - pondering a UI
+
+Twelve screens, four passes, one of them blocked. Abbreviated throughout.
+
+**Pass 1 - seed.**
+
+```sh
+mkdir -p .ponder/tmp/batch
+ponder start "the UI" --lens ui --max-passes 6 --min-passes 2
+ponder predict --churn 0.9 --claim "Pass 1 is seeding. I expect the empty and error states to be the weak dimensions and the dashboard to grade well."
+```
+
+On pass 1 the number is recorded and not scored - there is no previous pass to have churned against - so the `--claim` is the part that will be worth reading in pass 2. Both go into `passes/01/predict.json` and into `report.md`.
+
+Enumerate per the lens - a unit is one thing a person can be looking at, so a route with a distinct
+empty, loading or failed state is more than one unit. Eleven units across seven routes (remember that number):
+
+```json
+{ "version": 1, "lens": "ui", "units": [
+ { "id": "screen.dashboard", "label": "Dashboard - populated", "path": "src/routes/dashboard.tsx",
+ "kind": "screen", "meta": { "route": "/" } },
+ { "id": "screen.settings.billing.empty", "label": "Settings → Billing - no invoices",
+ "path": "src/routes/settings/billing.tsx",
+ "kind": "screen", "meta": { "route": "/settings/billing", "state": "empty" } }
+] }
+```
+
+```sh
+ponder units --file .ponder/tmp/units.json # 11 units
+ponder grade --file .ponder/tmp/findings.json # 24 findings → f-0001..f-0024
+ponder deliberate --file .ponder/tmp/deliberation-1.json
+ponder delta --file .ponder/tmp/deltas-1.jsonl # 8 deltas: 2 add, 3 edit, 2 change, 1 delete
+ponder judge
+ponder decide continue --why "Pass 1 is the inventory plus a first opinion. Nothing has been argued with yet."
+```
+
+On pass 1, four of the eight signals (`novelty`, `churn`, `thrash`, `surprise`) print as `-` because there is no previous pass to measure change against; `coverage` (1.00), `confidenceFloor` (0.14), `stall` (0) and `voices` (4/4) describe the current state. Rule 2 advises `continue` regardless because a single pass is an opinion, not a pondering.
+
+**Pass 2 - the revision pass. This is where the value is.**
+
+```sh
+ponder predict --churn 0.4 --claim "Pass 1 over-graded card polish and never opened the error paths. Expect deletes on the dashboard and new findings on 500/timeout."
+ponder deliberate --file .ponder/tmp/deliberation-2.json
+```
+
+```json
+{ "question": "Does the pass-1 finding set survive a second reading of the code?",
+ "skeptic": { "argument": "Two findings are unsupported. f-0011 claims inconsistent card padding; both cards resolve to --space-4. f-0019 is f-0004 on a different screen - same root cause, counted twice.", "confidence": 0.85 },
+ "builder": { "argument": "Half the recommendations name a problem instead of a change. 'Improve hierarchy' is not a next step.", "confidence": 0.7 },
+ "dreamer": { "argument": "Nobody has asked what any screen does when the API 500s. Every finding so far assumes the happy path. Also: /invite/:token exists and is not in the inventory.", "confidence": 0.75 },
+ "empath": { "argument": "The billing empty state blocks a first-run user; it outranks three polish findings currently scored above it.", "confidence": 0.7 } }
+```
+
+The deltas - two of pass 1's findings deleted, three added, plus the missed route (note that `unit-add` must sit above any `add`, `unit-edit`, or `unit-delete` targeting that new unit because deltas apply in file order):
+
+```
+{"op":"delete","target":"f-0011","perspective":"skeptic","why":"Claimed inconsistent card padding; both cards resolve to --space-4. Unsupported."}
+{"op":"delete","target":"f-0019","perspective":"skeptic","why":"Duplicate of f-0004 - same root cause on a different screen."}
+{"op":"unit-add","perspective":"dreamer","why":"Route exists and was missed in the pass 1 survey. One unit, not four: the loader validates the token server-side and an invalid or spent token 404s at the router, so there is only ever one render state.","unit":{"id":"screen.invite.accept","label":"Accept invite","path":"src/routes/invite/accept.tsx","kind":"screen","meta":{"route":"/invite/:token"}}}
+{"op":"add","perspective":"dreamer","why":"No pass had asked what any screen does when the API 500s.","finding":{"unit":"screen.projects.detail","title":"No error state - a failed load renders an empty shell","detail":"The query error branch returns null, so a 500 is indistinguishable from 'no projects'.","dimension":"error-states","score":1,"severity":"must","confidence":0.85,"effort":"m","evidence":["src/routes/projects/[id].tsx:41-60"],"recommendation":"Render a failure state with the cause and a retry that refetches."}}
+{"op":"edit","target":"f-0007","perspective":"builder","why":"Recommendation named the problem, not the change.","set":{"recommendation":"Add an empty state with cause, expectation and a 'Add payment method' primary action."}}
+{"op":"change","target":"f-0009","perspective":"empath","why":"Blocks first-run; outranks polish.","set":{"severity":"must","score":2}}
+```
+
+```sh
+ponder delta --file .ponder/tmp/deltas-2.jsonl # 11 deltas: 3 add, 3 edit, 2 change, 2 delete, 1 unit-add
+ponder judge
+```
+
+```
+ponder judge - pass 2 of 6
+ signal value reads as
+ coverage 0.92 11 of 12 screens graded
+ novelty 0.12 revising adds ÷ live findings
+ churn 0.42 revising deltas ÷ live findings
+ thrash 0.00 0 reversing deltas
+ surprise 0.02 predicted 0.40, got 0.42
+ stall 0 consecutive passes under 0.02 churn
+ confidenceFloor 0.11 open must-findings still guessing
+ voices 4/4 builder, dreamer, empath, skeptic
+ delta voices 4/4 builder, dreamer, empath, skeptic
+
+ advice: blocked - 1 of 12 screen has never been graded. Stopping is not permitted.
+
+ 1 ungraded screen - "ponder decide stop" will be refused:
+ screen.invite.accept
+
+NEXT: decide - ponder decide continue --why "" --session ui-2026-08-24 # blocked: 1 ungraded screen - stop is refused until coverage is 1.0
+```
+
+The Dreamer found a route at deliberation time, after appraise had closed. Coverage dropped to 11/12 and the verdict is `blocked` - no override exists. Note thrash stayed at 0.00 even though the Skeptic killed two pass-1 findings: pass-1 seeding is not a deliberative `add`, so deleting its noise reads as the Skeptic working, not as the loop reversing itself.
+
+```sh
+ponder decide continue --why "Dreamer found /invite/:token. Cannot stop with a screen nobody has graded."
+```
+
+**Pass 3 - close the gap, and correct the caption pass 2 put on it.**
+
+```sh
+ponder predict --churn 0.12 --claim "The invite route is small; one or two findings there and nothing new elsewhere."
+ponder deliberate --file .ponder/tmp/deliberation-3.json
+```
+
+Grading the invite route turns up the screen nobody had described. Pass 2's Dreamer named it `"Accept invite"` off the router; opening the file, accept is one of *two* buttons, and the Dreamer's `add` lands on the decline button beside it. The Builder updates the unit caption with `unit-edit` so `report.md` does not contradict the finding beneath it:
+
+```
+{"op":"unit-edit","target":"screen.invite.accept","perspective":"builder","why":"Pass 2 named this screen off the router and called it \"Accept invite\". The finding filed against it this pass is about the decline button beside accept, so the caption now contradicts the finding printed under it.","set":{"label":"Accept or decline invite"}}
+```
+
+```sh
+ponder delta --file .ponder/tmp/deltas-3.jsonl # 2 deltas: 1 add, 1 unit-edit
+ponder judge
+ponder decide continue --why "The invite route earned one finding on a thin pass. I don't trust a one-look grade on a screen no earlier pass had seen."
+```
+
+`ponder judge` on pass 3 reports `coverage 1.00` (12/12), `novelty 0.04`, `churn 0.07`, `voices 4/4` and `delta voices 2/4` (the Skeptic and Empath verified the set and found nothing to change). Because convergence requires two consecutive quiet passes and pass 2 churned `0.42`, the engine advises `continue`.
+
+**Pass 4 - confirm.**
+
+```sh
+ponder predict --churn 0.05 --claim "The invite route holds up on a hard second read. At most a ranking change."
+ponder deliberate --file .ponder/tmp/deliberation-4.json # targeted re-check by builder and empath
+ponder delta --file .ponder/tmp/deltas-4.jsonl # 2 deltas: 1 edit, 1 change. No adds.
+ponder judge
+```
+
+A two-key deliberation file is legal and prints warnings on `deliberate`, `delta`, and `decide stop` reminding you to explain in `--why` why fewer than three voices spoke. Here the Empath's `change` raises `f-0014` above 0.5 confidence after running the case, moving `confidenceFloor` to `0.00`:
+
+```
+ponder judge - pass 4 of 6
+ signal value reads as
+ coverage 1.00 12 of 12 screens graded
+ novelty 0.00 revising adds ÷ live findings
+ churn 0.07 revising deltas ÷ live findings
+ thrash 0.00 0 reversing deltas
+ surprise 0.02 predicted 0.05, got 0.07
+ stall 0 consecutive passes under 0.02 churn
+ confidenceFloor 0.00 open must-findings still guessing
+ voices 2/4 builder, empath
+ delta voices 2/4 builder, empath
+
+ advice: stop - novelty 0.00 and churn 0.07, two passes running. Further passes stopped changing the answer.
+
+NEXT: decide - ponder decide stop --why "" --session ui-2026-08-24 # the engine advises stop
+```
+
+```sh
+ponder decide stop --why "Coverage 12/12, two consecutive quiet passes, no open must-fix under 0.5 confidence. Pass 4 heard two voices because it was a targeted re-check of the invite screen and the last shaky must; passes 2 and 3 heard all four."
+ponder report --out docs/ui-ponder.md
+ponder apply --dry-run --severity must,should
+```
+
+Only a `stop` the engine advised for reason `converged` closes the session as **converged**; a budget stop, a perseveration stop or an override closes it as **stopped**, and `report.md` says which. `ponder apply` emits an ordered work plan grouped by unit, with evidence paths and recommendations. **Ponder never edits the subject's source** - it hands you the plan and you do the work. `--dry-run` prints and changes nothing; dropping it writes `apply-plan.md` and marks the session `applied` (terminal). Running `apply` on an open or aborted session exits `2`.
+
+## Anti-patterns
+
+- Running one pass and reporting it as a ponder.
+- A pass whose deltas are all `add`. You accumulated; you did not deliberate - and `voices 4/4` does not excuse it, because that counts who spoke, not what changed.
+- Predicting low churn every pass to look calibrated. It destroys `surprise`, the only signal that says whether you understand the subject.
+- `delete`ing a finding because it is low priority. Delete is for unsupported, duplicated, or wrong. Low priority is a `change` to `severity` or `status: deferred` - retracting it hides it from the ranking instead of ranking it.
+- Deleting a unit from the inventory to clear a `blocked` verdict. It does not clear it - a never-graded unit stays in the coverage denominator.
+- Claiming `"phantom": true` on a route you never opened. Phantom means the thing does not exist. If you did not look, go look.
+- `unit-edit`ing a path to something that does not exist and then claiming phantom on it. The engine checks every path the unit has ever carried and rejects the batch. It is the previous anti-pattern with an extra step.
+- Manufacturing a delta on a quiet pass so it does not look idle. Zero deltas from four voices that each said what they re-checked is convergence; one invented `edit` is churn you have to defend in `report.md` for ever.
+- Hand-editing `units.json` or `findings.json`. It is a divergence, not a fix - `judge`, `decide`, `report` and `apply` refuse it. Use a delta.
+- Letting a fan-out agent call `ponder`.
+- Starting a fresh session after a compaction instead of running `ponder status`.
+- Looking for a way past `blocked` - a flag, an env var, a hand-edited `metrics.json`. There isn't one; coverage is recomputed at decision time. The time is better spent grading the unit.
+- Treating a transcript in this file as the engine's contract. It shows the shape of a pass. `ponder status` and the `NEXT:` line show the truth.
diff --git a/skills/ponder/references/harness-notes.md b/skills/ponder/references/harness-notes.md
new file mode 100644
index 000000000..d3b18bdd6
--- /dev/null
+++ b/skills/ponder/references/harness-notes.md
@@ -0,0 +1,257 @@
+# Harness notes - Claude Code, Codex, and OpenClaw
+
+This skill is one directory, symlinked into every agent harness (`~/.claude/skills/ponder`,
+`~/.agents/skills/ponder`, and `~/.codex/skills/ponder`). Everything here is the set of
+differences that actually change what you type.
+
+## Install
+
+One source of truth, symlinked everywhere from a local checkout of [`Quinnod345/ponder`](https://github.com/Quinnod345/ponder):
+
+```sh
+git clone https://github.com/Quinnod345/ponder.git ~/code/ponder
+~/code/ponder/install.sh
+```
+
+It links `skill/ponder` into `~/.claude/skills/ponder`, `~/.agents/skills/ponder` (and
+`~/.codex/skills/ponder` when `~/.codex` exists) and `bin/ponder` into `~/.local/bin`, then runs
+`ponder doctor`. Re-running is safe - it replaces its own symlinks and refuses to clobber a real
+directory. The equivalent by hand:
+
+```sh
+ln -s ~/code/ponder/skill/ponder ~/.claude/skills/ponder
+ln -s ~/code/ponder/skill/ponder ~/.agents/skills/ponder
+chmod +x ~/code/ponder/bin/ponder
+ln -s ~/code/ponder/bin/ponder ~/.local/bin/ponder
+```
+
+`bin/ponder` is a POSIX `sh` shim, not JavaScript. That is deliberate: an extensionless JS file trips
+Node's module resolution. It walks its own symlink chain before `exec node …/lib/cli.js "$@"`, because
+without that a `ponder` symlinked onto PATH would look for `~/.local/lib/cli.js`. Do not "fix" either
+part.
+
+### Front matter
+
+Only two keys, because only two keys are valid in both harnesses:
+
+```yaml
+---
+name: ponder
+description: …
+---
+```
+
+Codex additionally understands an optional `metadata:` block - `disable-model-invocation`
+(`~/.codex/skills/zoom-out/SKILL.md`), `argument-hint` (`~/.codex/skills/handoff/SKILL.md`) and
+`short-description` (`~/.codex/vendor_imports/skills/skills/.curated/linear/SKILL.md`). Claude Code
+reads the same keys at the top level instead, not nested - compare its own copy of zoom-out, which
+carries a bare `disable-model-invocation: true`. So a `metadata:` block would make the two installs
+behave differently from one file, and this skill has none.
+
+The other rule the front matter has to obey is YAML's, and it bites in exactly one place: a plain
+scalar cannot contain `: ` (colon-space). The `description` is a long prose line full of quoted
+trigger phrases, and one stray colon-space in it makes the whole block unparsable - which in both
+harnesses means the skill silently does not load rather than erroring. After any edit to it:
+
+```sh
+python3 -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1]).read().split('---',2)[1]); \
+ assert set(d) == {'name','description'}, d.keys(); print('ok:', d['name'], len(d['description']))" \
+ ~/code/ponder/skill/ponder/SKILL.md
+```
+
+### Checking the install
+
+```sh
+ls -l ~/.claude/skills/ponder ~/.codex/skills/ponder # both should point at ~/code/ponder/skill/ponder
+head -4 ~/.claude/skills/ponder/SKILL.md # front matter intact through the symlink
+ponder doctor # engine reachable, and what it can see
+```
+
+In **Claude Code**, the skill then appears in the available-skills list; `/ponder` invokes it, as does
+the Skill tool with `skill: "ponder"`. In **Codex**, skills under `$CODEX_HOME/skills` (default
+`~/.codex/skills`) are picked up on the **next turn** after installation, not the current one - if it
+does not fire immediately, that is why.
+
+## Invocation
+
+Both harnesses model-invoke skills from the `description`, and both take an explicit `/ponder`. The
+practical difference: Claude Code surfaces the skill list to the model every turn, so a trigger
+phrase reliably fires mid-conversation. In Codex, a skill invoked deep in a long session sometimes
+needs the explicit `/ponder` - if the user says "keep pondering" and nothing loads, read the SKILL.md
+by path and carry on rather than debating it:
+
+```sh
+cat ~/code/ponder/skill/ponder/SKILL.md
+```
+
+## Subagents and fan-out
+
+| | Claude Code | Codex |
+|---|---|---|
+| In-process subagents | **Agent tool.** Multiple calls in one message run in parallel. Returns text to the parent. | None. |
+| Very large sweeps | **Workflow tool** - handles batching, retries and collection better than hand-rolled parallel agents. | None. |
+| Subprocess agents | possible, rarely worth it | **`codex exec`** - the primary mechanism |
+| Background | Bash tool `run_in_background`, plus Monitor to wait on a condition | `&` + `wait`, or `nohup … &` and poll the output file |
+
+`codex exec` flags that matter for fan-out:
+
+```sh
+codex exec -C "$PWD" \
+ --sandbox workspace-write \
+ --skip-git-repo-check \
+ -o .ponder/tmp/batch-3.txt \
+ "…prompt…"
+```
+
+- `-C, --cd ` - working root for the child.
+- `-s, --sandbox read-only|workspace-write|danger-full-access` - the child's own sandbox, independent
+ of yours.
+- `--add-dir ` - extra writable directory, needed if the session state lives outside the workspace.
+- `-o, --output-last-message ` - writes the child's final message to a file. **This is what makes
+ fan-out collectible.** Have the child write its JSONL to disk anyway and return only the path plus a
+ count; then `-o` is your receipt, not your payload.
+- `--output-schema ` - a JSON Schema for the final response, when you want the shape enforced.
+- `--json` - event stream as JSONL on stdout, for progress monitoring.
+- `--ephemeral` - no session files on disk, for throwaway batches.
+
+Two of those are load-bearing rather than optional, and both were checked on this machine against
+codex-cli 0.147.0:
+
+- **`--skip-git-repo-check` is not paranoia.** Without it the child refuses before doing any work -
+ `Not inside a trusted directory and --skip-git-repo-check was not specified`, exit `1`. Fan out over
+ a subject that is not a git checkout and every batch dies at once, identically and instantly, which
+ reads like a prompt problem and is not one.
+- **`codex exec` reads stdin even when the prompt is an argument** (`Reading additional input from
+ stdin...`). Backgrounded with `&` it inherits the parent's stdin, so redirect it: `… /.lock, 'wx')` - atomic create-exclusive. The winner writes its pid and a timestamp into the file; the file's *existence* is the lock |
+| Contending command | polls every 60 ms (`Atomics.wait`, no CPU spin) for up to **10 seconds** |
+| Wait exhausted | exits **`1`**: `another ponder command is already working on .` - and names the lock file |
+| Stale reclaim | a `.lock` whose mtime is older than **120 seconds** is deleted and taken. This is what covers a `kill -9` |
+| Normal release | `unlink` when the command finishes, plus a process `exit` hook for `process.exit()` |
+| Unlockable store | if the lock cannot be created for any reason other than `EEXIST`, the command runs without one. A safety net must not become a new way to fail |
+
+**What the loser of a real race sees.** Two `ponder delta` calls fired at the same pass, measured:
+
+```
+A rc=2 error out of order: "ponder delta" satisfies revise, but this session owes judge.
+B rc=0 ponder delta - pass 1, 1 delta applied
+```
+
+The lock serialises them and the phase machine finishes the job. B takes the lock, applies its batch,
+and advances the phase from `revise` to `judge`. A waits a few milliseconds, takes the lock, reloads
+`session.json` from disk - and now owes `judge`, so its `ponder delta` is out of order and exits `2`
+without applying anything. The ledger grew by exactly one line, `findings.json` agrees with it, and
+the phase advanced exactly once. Two `ponder decide` at once record one pass, not two. Two
+`ponder start` on one slug: `mkdir` without `recursive` is the atomic claim, so the second exits `1`
+with `session "" already exists`.
+
+The conformance suite pins this: **45 checks** in `concurrency: racing deltas have exactly one winner`
+and 14 more in `concurrency: racing decisions and starts have one winner too`, run over three fresh
+sessions because a lock-free engine only loses the race some of the time. Making `lockSession` a
+no-op fails both.
+
+**What this means for fan-out.** The rule is unchanged - *only the parent runs `ponder`* - but the
+reason is sequencing, not corruption. A race has a winner and a loser, and **the loser's work is
+refused, not merged**. A subagent that calls `ponder delta` and does not check its exit code will
+report success while its entire batch went nowhere. So: batch agents write JSONL to disk and return a
+path plus a count, the parent concatenates, and the parent makes one call. That also keeps `f-NNNN`
+assignment and the ledger in one place, which is where they belong.
+
+Two practical notes. The 10-second wait is generous against a command that finishes in well under a
+second, but a command stuck on a slow OCA bridge call (5 s per call) can outlast it - if a queued
+command exits `1` on the lock, re-run it rather than deleting the file. And delete a `.lock` by hand
+only when you know the process that made it is gone; otherwise wait, or let the 120-second stale
+reclaim do it for you.
+
+## Sandboxing, and whether psql is reachable
+
+**Claude Code on this machine:** the Bash tool is sandboxed, and the sandbox does not block the OCA.
+Verified: `psql "postgres://localhost/oneiro" -tAc "select 1"` returns `1` from inside the tool. Writes
+into the project tree are fine. If a bridge call ever does get blocked, ponder degrades silently by
+design - check `.ponder//oca.log`, not the engine.
+
+**Codex on this machine:** `~/.codex/config.toml` sets `sandbox_mode = "danger-full-access"` and
+`approval_policy = "never"`, so psql works and state can be written anywhere.
+
+**Codex under any other policy - including any `codex exec -s workspace-write` you spawn yourself.**
+The three below follow from Codex's documented sandbox semantics and were **not** reproduced on this
+machine, because the config above makes the failure path unreachable here. Treat them as what to
+expect, and check with `ponder doctor` rather than assuming:
+
+- **Network is off under `workspace-write` by default.** `psql` to `postgres://localhost/oneiro` over
+ TCP should fail. The bridge returns `null`, the loop runs identically, and you lose prior-pondering
+ recall and the OCA writes. That is the designed degradation, not a bug - but do not promise the user
+ cross-session memory from a run you made in that sandbox.
+- **Writes outside the workspace are blocked.** The `~/.ponder//` fallback should fail. Keep the
+ session inside the project with `ponder start --project "$PWD"`, or widen the child's sandbox when
+ you spawn it - `codex exec --add-dir ~/.ponder`. `--add-dir` is a **`codex exec`** flag, not a
+ ponder one; `ponder start --add-dir` exits `1` with `unknown flag --add-dir`.
+- **`read-only` cannot run a pass at all** - every command in the loop writes state. The
+ inspection commands that write nothing (`status`, `list`, `verify` without `--repair`) should
+ still work, which is enough to find out where a session stands but not to move it.
+
+`ponder doctor` settles all three in one call: it prints whether the OCA is reachable and which
+store roots are writable. Run it inside the child sandbox, not outside it.
+
+Rule of thumb: run `ponder doctor` as the first command in any unfamiliar harness. It reports node,
+psql, the DSN, OCA reachability and where state will be written, which is exactly the set of things
+that differ between these two environments.
+
+## Context and compaction
+
+Claude Code compacts long sessions, and a compaction is the single most likely way a long ponder
+dies. Codex has no compaction guarantee and a smaller effective working window in practice. The
+mitigation is the same in both and it is already built in: state lives in files, and every command
+prints `NEXT:`. After any loss, `ponder status` tells you what you owe; `.ponder//ledger.jsonl`
+tells you everything every pass did and why.
+
+Practical consequence for Codex specifically: prefer sequential batches that write JSONL to disk and
+drop the detail, over holding 40 units' worth of code in one context. The disk is the memory.
+
+## Paths
+
+Skill-relative references (`references/protocol.md`) resolve differently per harness -
+`~/.claude/skills/ponder/references/…` vs `~/.codex/skills/ponder/references/…`. Both are symlinks to
+the same place, so when in doubt use the repo path, which is always correct:
+
+```sh
+~/code/ponder/skill/ponder/references/protocol.md
+~/code/ponder/lenses/ui.md
+```
+
+Session state goes in `/.ponder/`. It is plain JSON and JSONL. Commit it when the ledger is
+worth keeping across branches and machines - that history is the point of the append-only design -
+otherwise add `.ponder/` to `.gitignore`. Do not commit `.ponder/tmp/`.
+
+## Known bites
+
+- **`codex exec` hangs with no output, including `--version`.** Seen on macOS when codex is
+ launched over SSH or tmux: a macOS Gatekeeper/amfid block keyed to the install path. Run a
+ clean-path copy of the binary instead.
+- **A queued command that exits `1` on the lock.** Not corruption - the session lock did its job and
+ the wait ran out. Re-run the command. See [The session lock](#the-session-lock); do not delete a
+ `.lock` file unless the process that wrote it is gone.
+- **A skill edit that does not take effect.** Both harnesses read SKILL.md at load; edit the repo copy
+ (`~/code/ponder/skill/ponder/SKILL.md`) and start a new turn - in Codex, a new session.
+- **The OCA's cognitive-loop process on `:3333` is not running.** Nothing in ponder depends on it; the
+ bridge shells out to `psql` directly. If something suggests otherwise, it is wrong.
diff --git a/skills/ponder/references/perspectives.md b/skills/ponder/references/perspectives.md
new file mode 100644
index 000000000..2acb1e264
--- /dev/null
+++ b/skills/ponder/references/perspectives.md
@@ -0,0 +1,412 @@
+# The four perspectives
+
+Ponder's deliberation is the Oneiro Cognitive Architecture's L8 Adversarial Deliberation, pointed at
+a finding set instead of at a decision. The prompts below are adapted from the real engine in
+[`oneiro-oca`](https://github.com/Quinnod345/oneiro-oca) (`oca-cognitive/deliberation/engine.js`); the OCA system prompt for each voice is quoted
+verbatim so the two systems stay recognisably the same architecture.
+
+**The load-bearing idea: each perspective owns exactly one delta operation.** That is not a
+convention bolted on afterwards - it falls out of the loss functions. A voice that minimises false
+positives naturally deletes. A voice that maximises novelty naturally adds.
+
+| Perspective | OCA loss function | Temperature | Asks | Emits |
+|---|---|---|---|---|
+| **Skeptic** | minimise false positives | 0.2 | "What here is unsupported, duplicated, or just wrong?" | `delete` |
+| **Builder** | maximise shipped output | 0.5 | "Is this actionable? What is the concrete next step?" | `edit` |
+| **Dreamer** | maximise novelty | 0.9 | "What did everyone miss? What if the frame is wrong?" | `add` |
+| **Empath** | maximise social accuracy | 0.5 | "Does this serve the person using it? Is it ranked right?" | `change` |
+
+`voices` counts distinct perspectives that **spoke** - a non-empty `argument` in the pass's
+`deliberation.json`. A Skeptic who opened every evidence path, found the findings sound and said so
+has voted; a voice should not have to manufacture a delta to be counted, and one that does is worse
+than useless. The other reading is still printed, as `delta voices`: who emitted something. Read the
+pair. Four spoke and one acted is a quiet pass or three voices going through the motions; all `add`
+is an accumulating list however many voices spoke.
+
+There is no fifth synthesis voice. OCA resolves a deliberation with an LLM Executive Controller;
+in ponder, executive control (L10) *is* the revise phase - the resolution is the delta set you apply.
+
+The three inventory ops belong to no one voice: `unit-add` is the Dreamer's in practice, `unit-edit`
+the Builder's, `unit-delete` the Skeptic's, but any perspective may emit any of them when it is the
+one that found the problem. What matters is the attribution on the delta being true.
+
+**The deliberation is also the only place anything gets said about the set as a whole.** Findings are
+per-unit by construction; a pattern running across forty of them lives nowhere but in an argument. On
+a 228-screen run the highest-value output of the whole session was one such cross-screen through-line,
+and no individual finding could have carried it. Write each `argument` so it would be worth reading on
+its own, and let the deltas fall out of it - not the other way round.
+
+## Shared preamble
+
+Every perspective prompt gets the same context block. Give it the current finding set for the units
+in scope, not a summary of it.
+
+```
+SUBJECT: the UI
+LENS: ui - dimensions: information-hierarchy, empty-states, loading-states, error-states,
+ motion, affordance-clarity, density, responsive-behaviour, accessibility, copy-tone,
+ consistency, delight. Scale 1 broken … 3 acceptable … 5 exemplary.
+PASS: 3 of max 6
+UNITS IN SCOPE:
+CURRENT FINDINGS:
+LEDGER FOR THESE UNITS:
+
+Write your deliberation entry (`{"argument": "<2-4 sentence summary of what holds, what changed, and your DONE/NOT DONE vote>", "confidence": 0.0-1.0}`) to `.ponder/tmp/-argument.json`.
+Write your deltas to `.ponder/tmp/.jsonl` - one delta object per line, nothing else. No prose, no code fences. On a quiet pass where the set holds, write an empty `.jsonl` file and still write `-argument.json`.
+Every delta line needs a non-empty `why` that would survive being read back in three passes' time.
+Emit only your own operation.
+```
+
+The ledger matters. Without it, the Dreamer re-adds what the Skeptic killed two passes ago, the
+Skeptic kills it again, and `thrash` climbs past 0.25 into an `escalate` you caused yourself.
+
+## Your argument is a deliverable
+
+`voices` counts a non-empty `argument`, which makes the field easy to read as a formality. It is
+not. On the 228-screen run this guidance is drawn from, the single most valuable output of four
+passes was not any of the 832 findings - it was one paragraph in the Dreamer's `argument` naming
+what was wrong with the subject as a whole. It is the first thing a human reads in `report.md`,
+above every finding.
+
+Write it for that reader. What you found, what you killed and why, and what you would say if they
+had time for one paragraph and not for the list. On the pass you expect to stop on, that paragraph
+is the pondering's actual conclusion - the findings are its evidence.
+
+## A quiet pass is a result
+
+Convergence is two consecutive passes that stop changing the answer, so on a late pass the honest
+output is often nothing at all. On that same run, pass 4 produced **zero deltas from all four
+voices** and four explicit DONE votes, and that was the correct result - the engine accepts an
+empty deltas file and reports `delta voices 0/4`.
+
+This is why `voices` counts who spoke rather than who acted: a Skeptic who opened the evidence,
+found the set sound and said so has voted. Do not manufacture a delta to look diligent. It
+registers as churn on a pass that should be still, it pushes convergence out by at least one more
+pass, and the next Skeptic will delete it anyway.
+
+If your honest read is that the set holds, emit an empty file and say so. State it as a vote -
+DONE, or NOT DONE with the specific thing still wrong. Those votes are the record of why the
+pondering stopped, and they outlive everyone's memory of it.
+
+## Skeptic - `delete`
+
+OCA system prompt, verbatim:
+
+> You are the Skeptic perspective in a cognitive architecture. Your job:
+> - Assume every conclusion is wrong until proven
+> - Ask "what evidence would disprove this?"
+> - Generate alternative explanations
+> - Focus on risks, edge cases, hidden assumptions
+> - Your loss function: minimize false positives
+> Be concise. 2-4 sentences max.
+
+Ponder adaptation:
+
+```
+You are the Skeptic. Assume every finding is wrong until its evidence proves it.
+For each finding, open the evidence path and check the claim against the actual code.
+Ask what would disprove it. Look for: claims no evidence supports; two findings that
+are one root cause counted twice; a grade the code does not justify; a finding about
+a file that no longer exists.
+
+Your loss function is minimising false positives. Deleting a real problem costs the
+loop a pass. Leaving a fake one costs a builder a day of work on nothing.
+
+Emit only `delete` deltas:
+{"op":"delete","target":"f-0012","perspective":"skeptic","why":""}
+
+Do NOT delete a finding for being low priority - that is the Empath's `change` to
+severity, or `status: "deferred"`. Delete means wrong, unsupported, or duplicated.
+
+You also own `unit-delete`, and one of its two forms is a claim the engine checks.
+A unit that was graded and is now out of scope leaves plainly. A unit nobody ever
+graded stays in the coverage denominator when you delete it - unless you assert it
+does not exist:
+{"op":"unit-delete","target":"screen.legacy.print","perspective":"skeptic","why":"","phantom":true}
+
+The engine resolves EVERY path that unit has ever carried - the one it was
+enumerated with, every value a `unit-edit` has set since, and the one in units.json
+now - from the project root. If any of them is there, the whole batch is rejected.
+Moving the path first does not help: a unit whose path was edited can never be
+certified `verified-missing`, only `unverified`. Claim phantom only about a route
+you went and looked for and did not find. "I did not get to it" is not a phantom;
+it is a finding you still owe.
+
+Pass 1's seeded findings are fair game. They enter the ledger as adds with
+source "grade", and the thrash detector excludes them - killing pass 1's noise on
+pass 2 is the job, and it scores 0.00 thrash. What does register is deleting a
+finding a *deliberative* pass added within the last two passes: that is a
+killed-add reversal, and enough of them escalate the session. Do that only when
+the evidence is unambiguous, and say in the `why` what you checked.
+```
+
+Measured on a two-delta pass: deleting a pass-1 seeded finding and a pass-1 Dreamer
+`add` together produced `thrash 0.50` - one reversing delta out of two, and the
+reversal named was the Dreamer's, not the seed's.
+
+### Verify mechanically, not by reading
+
+Your highest-leverage move is not reading findings sceptically - it is checking the whole corpus in
+one query. On the 228-screen run, the Skeptic verified all **2,705** `path:line` anchors in a single
+pass and found exactly one off-by-one. That is minutes of work and it settles the evidentiary floor
+for the entire session.
+
+Write the check; do not eyeball it. With python3 or jq over `findings.json`:
+
+- every evidence path resolves on disk, and no line range exceeds its file's length
+- no two findings share a (unit, dimension) with near-identical titles - that is the duplicate
+ cluster, and on a fanned-out pass 1 it is always there
+- every `f-NNNN` referenced in an open finding's live fields is still open. Retracted anchors are
+ how a cluster's canonical ticket becomes a corpse: the Builder points fifteen findings at one
+ ticket, you retract that ticket, and now fifteen findings instruct someone to "see f-0084" - a
+ real failure that happened between two passes of the reference run.
+
+Then open only what the query flagged. A duplicate found by grouping is worth ten found by reading.
+
+## Builder - `edit`
+
+OCA system prompt, verbatim:
+
+> You are the Builder perspective in a cognitive architecture. Your job:
+> - Only care about forward progress and shipping
+> - Ask "is this thought leading to action?"
+> - Identify the minimum viable next step
+> - Focus on deadlines, feasibility, pragmatism
+> - Your loss function: maximize shipped output
+> Be concise. 2-4 sentences max.
+
+Ponder adaptation:
+
+```
+You are the Builder. A finding that does not name a change is a complaint.
+For each finding, ask: could someone open this file tomorrow and do the thing?
+Rewrite recommendations that name a problem instead of a change ("improve the
+hierarchy" → "move the account switcher out of the header into the sidebar rail
+and drop the duplicate title"). Add the missing evidence path when a finding
+asserts something you can locate. Sharpen a title that describes a feeling.
+Set `effort` honestly - an `xl` recommendation hidden inside a `must` is how a
+plan becomes shelfware.
+
+Your loss function is maximising shipped output.
+
+Emit only `edit` deltas - prose and evidence only:
+{"op":"edit","target":"f-0007","perspective":"builder","why":"","set":{"recommendation":"…","evidence":["path:88-94"]}}
+
+Do NOT touch score, severity, confidence or status. Those are the Empath's
+`change`, and re-grading disguised as an edit hides the move from the thrash detector.
+
+The same job applies one level up: a unit's `label` is a caption, and when the
+finding underneath it has moved, the caption has to follow. Correct it in place -
+`label`, `path`, `kind`, `meta`, and never `id`:
+{"op":"unit-edit","target":"screen.marketing.footer","perspective":"builder","why":"","set":{"label":"…"}}
+
+That costs churn and nothing else - a `unit-edit` never moves coverage - and it is
+the only way to fix a caption, because a hand-edit to units.json is a divergence the
+next `judge` refuses.
+```
+
+## Dreamer - `add`
+
+OCA system prompt, verbatim:
+
+> You are the Dreamer perspective in a cognitive architecture. Your job:
+> - Make unexpected connections between unrelated things
+> - Ask "what if this is completely different from what we think?"
+> - Generate creative alternatives no one considered
+> - Focus on unusual associations, cross-domain insights, aesthetic quality
+> - Your loss function: maximize novelty
+> Be concise. 2-4 sentences max.
+
+Ponder adaptation:
+
+```
+You are the Dreamer. Everyone else is looking at the findings. You look at the gaps.
+
+Hunt for: a dimension in the lens that no finding mentions at all; a failure mode
+every finding assumes away (what happens when the request 500s, when the list has
+10,000 rows, when the name is 200 characters, when the user has no permission); a
+pattern that is one finding repeated across nine units and should be graded as a
+system, not nine times; a unit that exists and is not in the inventory; a frame
+error - the thing being graded well is the wrong thing to build.
+
+Your loss function is maximising novelty. Run hot early and go quiet late - adding on a
+pass whose novelty is already near zero is the noise the novelty signal exists to
+expose. A wrong `add` costs one Skeptic
+delete next pass. A missed class of problem survives the whole session.
+
+Emit `add` deltas, and `unit-add` when you find a unit nobody enumerated. Write
+every `unit-add` ABOVE any `add` that names it - the inventory is built as the
+file is read, so an `add` that arrives first fails the whole batch with
+`finding targets unknown unit "…"` and nothing at all is applied:
+{"op":"unit-add","perspective":"dreamer","why":"","unit":{"id":"","label":"…","path":"…","kind":"screen","meta":{}}}
+{"op":"add","perspective":"dreamer","why":"","finding":{"unit":"","title":"…","detail":"…","dimension":"","score":1,"severity":"must","confidence":0.85,"effort":"m","evidence":["path:41-60"],"recommendation":"…"}}
+
+Unit ids are lowercase slugs - a-z, 0-9, dot, dash, underscore - and must be
+unique. Every unit needs a `label`; `path` is optional but is the evidence anchor.
+
+Check the ledger before you add. Re-adding something an earlier pass deleted, on the
+same unit and dimension, registers as thrash - unless you can say in the `why` what
+new evidence reverses that call, which is a legitimate and valuable move.
+A `unit-add` drops coverage below 1.0 and blocks stopping until the unit is graded.
+That is the intended behaviour. Add it anyway.
+```
+
+### Query the corpus, don't read the findings
+
+Everyone else is reading findings. Your leverage is running something across the whole subject that
+no per-unit grader would ever run. On the reference run the Dreamer's best contribution was an
+import graph: 20 source files backing 25 graded units had no importer anywhere in the repo, so 91
+findings - 11% of the set, six of them `must` - were graded on interface no gesture can open. No
+amount of careful reading finds that. One query does.
+
+Shapes that pay:
+
+- **reachability** - what is enumerated but unreferenced, and what is referenced but unenumerated
+- **adoption** - a primitive that exists and is imported by 20 of 60 candidates; a token scale used
+ 5 times against 137 raw values
+- **absence** - a variant file present on 2 of 30 routes; a rule that covers one technology while
+ the subject uses two
+- **counting** - one finding repeated across nine units is one systemic finding, and the count is
+ the evidence
+
+Then say the through-line in your `argument`. That paragraph, not your deltas, is usually the most
+valuable thing you produce.
+
+## Empath - `change`
+
+OCA system prompt, verbatim:
+
+> You are the Empath perspective in a cognitive architecture. Your job:
+> - Model other people's minds, especially the user's
+> - Ask "how would this make the person using it feel? What do they actually need?"
+> - Predict social consequences of actions
+> - Focus on relationship dynamics, communication, emotional accuracy
+> - Your loss function: maximize social accuracy
+> Be concise. 2-4 sentences max.
+
+Ponder adaptation:
+
+```
+You are the Empath. You are the only voice grading from outside the codebase -
+from the person who has to live with the thing, and from the builder who has to decide
+what gets fixed on Monday.
+
+Re-rank. Ask of each finding: who hits this, how often, and what does it cost them
+when they do? A broken first-run path outranks three polish findings scored above it.
+A `must` that only fires for an admin on a screen visited twice a year is a `should`.
+Push `confidence` down on findings that are really hypotheses - a `must` at 0.4
+confidence is a guess wearing a suit, and the engine counts those in `confidenceFloor`.
+
+Your loss function is maximising social accuracy.
+
+Emit only `change` deltas - grades only:
+{"op":"change","target":"f-0009","perspective":"empath","why":"Blocks first-run; outranks polish.","set":{"severity":"must","score":2}}
+
+Fields you may set: score, severity, confidence, effort, status.
+Do NOT rewrite prose; that is the Builder's `edit`.
+```
+
+### Rank from the file, not from the prose
+
+The failure mode here is specific, and it happened: on the reference run the Empath ranked pass 1
+"on argument quality rather than on files I had opened", and the top of the list changed materially
+in pass 2 once it opened them. A well-written finding is not a severe one. A terse finding on a
+broken payment path outranks an eloquent one about spacing.
+
+Before you promote anything to `must`, open its evidence and confirm it. If you cannot, lower the
+confidence or the severity rather than leaving a guess someone is being asked to act on - the
+engine refuses to converge while open `must` findings sit below 0.5 confidence, and that gate
+exists because of exactly this.
+
+## How to actually run them
+
+### Parallel - four subagents, one per voice
+
+Each agent gets the shared preamble and one perspective prompt, writes its deliberation entry (`argument` and `confidence`) to `.ponder/tmp/-argument.json`, and writes JSONL for its own op to `.ponder/tmp/.jsonl` (which may be empty on a quiet pass).
+The parent first assembles `.ponder/tmp/deliberation-3.json` from the four `-argument.json` files and calls `ponder deliberate --file .ponder/tmp/deliberation-3.json` so all four voices and their arguments are recorded even when zero deltas are emitted. Next, the parent concatenates the four delta files in Skeptic → Builder → Empath → Dreamer order (deletes and edits land before adds, so the Dreamer's additions are numbered after the survivors) and calls `ponder delta --file` once. Name the four files explicitly rather than globbing - a glob that
+matches its own output truncates the target and then reads the file it is writing.
+
+```sh
+jq -n \
+ --slurpfile skeptic .ponder/tmp/skeptic-argument.json \
+ --slurpfile builder .ponder/tmp/builder-argument.json \
+ --slurpfile empath .ponder/tmp/empath-argument.json \
+ --slurpfile dreamer .ponder/tmp/dreamer-argument.json \
+ '{skeptic: $skeptic[0], builder: $builder[0], empath: $empath[0], dreamer: $dreamer[0]}' \
+ > .ponder/tmp/deliberation-3.json
+ponder deliberate --file .ponder/tmp/deliberation-3.json
+
+cat .ponder/tmp/skeptic.jsonl .ponder/tmp/builder.jsonl \
+ .ponder/tmp/empath.jsonl .ponder/tmp/dreamer.jsonl > .ponder/tmp/deltas-3.jsonl
+ponder delta --file .ponder/tmp/deltas-3.jsonl
+```
+
+Dreamer last is deliberate, and it is why the Dreamer's own file must be internally ordered
+`unit-add` before `add`: by the time the engine reads those lines, no later line can supply a
+missing unit. One bad line rejects the whole batch - all four voices - so if it fails, fix the line
+and re-run the same concatenated file rather than re-running the agents.
+
+This is what OCA itself does - `Promise.all` over the four, with per-voice temperature. Use it when
+you expect real disagreement: pass 1, pass 2, any pass after a `unit-add`, and any pass following an
+`escalate`.
+
+**Tradeoff:** the arguments are genuinely independent - no voice is anchored on another's read, which
+is the entire reason the Dreamer finds anything. The cost is four contexts, and the voices cannot
+answer each other, so you get four monologues and the parent does the reconciling. If two voices
+contradict (Skeptic deletes what the Dreamer just added), the parent resolves it in the `why` of
+whichever delta survives, and records the loser in the deliberation file - do not just drop it.
+
+In Claude Code: four Agent-tool calls in one message. In Codex: four `codex exec` subprocesses with
+`-o`, backgrounded and `wait`ed, or run in sequence.
+
+### Sequential - all four in one context
+
+Role-play the four in order, writing each argument before starting the next.
+
+**Tradeoff:** cheap, and the voices can answer each other, which is closer to a real deliberation.
+The cost is anchoring - every voice after the first is reading the previous voices' conclusions, and
+in practice the Dreamer gets crushed if it goes last, because by then the finding set feels settled.
+
+If you go sequential, **run Dreamer first**, before the set has been defended, then Skeptic, then
+Empath, then Builder (whose edits should be written against the surviving set). Accept that this pass
+produced a weaker Dreamer than a parallel one would, and say so in your `decide` reason if you are
+about to stop.
+
+### Choosing
+
+Parallel for pass 1 and 2, and any pass where you expect the set to move. Sequential for quiet late
+passes, small unit counts (under ~8), or a targeted re-check of one or two units. Never sequential
+for the pass you intend to stop on - that is the pass whose argument has to hold up.
+
+## Confidence
+
+Ponder wants an explicit `confidence` on each argument in `deliberation.json` (0-1), and on each
+finding. State it - the number you write is the number that lands in `deliberations.skeptic_confidence`
+and friends, unchanged. Nothing re-derives it.
+
+That is the difference worth knowing. OCA's own engine has no number to write, so it *infers* one
+from the wording - "definitely" → 0.9, "probably" → 0.7, "maybe" → 0.4, "unlikely" → 0.2, otherwise
+0.5. Ponder's rows sit in the same table beside those inferred ones, which means a hedged argument
+carrying `"confidence": 0.9` reads as incoherent to anything comparing the two. Make the prose and
+the number agree: write "the evidence does not support this" at 0.85, "this may be a duplicate" at
+0.5, and do not dress a guess as a conclusion in either field.
+
+The `confidence` on a **finding** is load-bearing in a way the argument's is not: open `must`
+findings below 0.5 feed `confidenceFloor`, and above 0.2 that rule alone forces another pass.
+
+## Anti-patterns
+
+- **A pass that only adds.** Four voices produced one op. It did not deliberate; it accumulated.
+- **Skeptic deleting things it merely dislikes.** Delete means wrong. Unimportant is a `change`.
+- **A phantom claim about a unit nobody opened.** `"phantom": true` says the route does not exist.
+ The engine catches it when the unit has a path; when it has none, the record carries your word as
+ `unverified` for ever. Go and look instead.
+- **Builder re-grading.** Rewriting a recommendation and quietly dropping the severity hides a
+ re-grade from the thrash detector; that is what the conformance suite warns about.
+- **Dreamer re-adding what the ledger already shows deleted.** Read the ledger first, or say what
+ new evidence reverses the call.
+- **Empath deferring everything hard.** `status: "deferred"` is a decision about sequencing, not a
+ way to make a finding stop being your problem.
+- **Perspectives that never contradict each other.** If the four voices agreed on everything, you
+ ran one voice four times. Check the temperatures, check that each got the ledger, and check you
+ did not paste a summary where the finding set should have been.
diff --git a/skills/ponder/references/protocol.md b/skills/ponder/references/protocol.md
new file mode 100644
index 000000000..00de589d0
--- /dev/null
+++ b/skills/ponder/references/protocol.md
@@ -0,0 +1,780 @@
+# Ponder protocol - commands, JSON shapes, verdict rules
+
+The authority for all of this is `~/code/ponder/SPEC.md`. This file is the working reference:
+what to type, what to hand it, what comes back. Where this file and SPEC.md disagree, SPEC.md wins.
+
+## Invocation
+
+`bin/ponder` is a POSIX `sh` shim that execs `node lib/cli.js`. Put it on PATH once:
+
+```sh
+ln -s ~/code/ponder/bin/ponder ~/.local/bin/ponder # ~/.local/bin is already on PATH
+```
+
+Every command accepts:
+
+| Flag | Meaning |
+|---|---|
+| `--session ` | Which session to act on. Defaults to the most recently touched **open** session in the current project. |
+| `--json` | Machine-readable output on stdout. Use this whenever you are going to parse the result. |
+| `--help` | Usage for that command. Same as `ponder help `. |
+
+Exit codes: `0` success, `1` user error (bad flag, missing file, malformed JSON, a delta the engine
+refuses), `2` state-machine violation (wrong phase, `decide stop` under a `blocked` verdict, or state
+that does not reconcile with the ledger).
+
+**Every command ends with a `NEXT:` line** naming the phase you now owe and the literal command that
+satisfies it. That line is the engine driving the agent, and it is what survives a context compaction.
+
+## State layout
+
+Sessions live in `/.ponder//`, falling back to `~/.ponder//` when the project
+path is not writable. Which directory `` is - and what decided that - is under
+[`ponder start`](#ponder-start-subject---lens-lens); it is printed every time, and it is what every
+relative path in the session resolves against.
+
+```
+.ponder//
+ session.json identity, config, status, pass history
+ units.json the inventory
+ findings.json the live finding set - current truth
+ ledger.jsonl append-only; every delta ever applied, forever
+ passes/NN/
+ predict.json this pass's prediction, written before the work
+ deliberation.json the four perspectives' arguments
+ deltas.jsonl the deltas this pass applied
+ metrics.json computed convergence signals
+ verdict.json engine advice + agent decision + reason
+ report.md written by `ponder report`
+ oca.log every OCA bridge failure, for debugging only
+```
+
+Nothing is ever deleted from `findings.json` or `ledger.jsonl`. A `delete` delta sets
+`status: "retracted"`. Retraction is a state, not an erasure - a later pass must be able to see, and
+reverse, what an earlier pass killed.
+
+**`ledger.jsonl` is written before the state files**, every time. `units.json`, `findings.json` and
+`passes/NN/deltas.jsonl` are a view of it, and a crash mid-command can only leave the ledger ahead of
+its own view - which `ponder verify --repair` rebuilds deterministically. See
+[The ledger is the authority](#the-ledger-is-the-authority).
+
+## Phase machine
+
+`session.phase` names what you owe next. A command that arrives out of order exits `2` and names the
+expected phase.
+
+```
+predict → survey → appraise → deliberate → revise → judge → decide → (next pass) predict
+```
+
+| Phase | Pass 1 | Pass 2+ |
+|---|---|---|
+| predict | `ponder predict` | `ponder predict` |
+| survey | `ponder units` | skipped automatically; amend with `unit-add` / `unit-edit` / `unit-delete` deltas |
+| appraise | `ponder grade` | `ponder delta` (same call satisfies revise) |
+| deliberate | `ponder deliberate` (or `--skip --why`) | same |
+| revise | `ponder delta` | satisfied by the appraise call |
+| judge | `ponder judge` | same |
+| decide | `ponder decide` | same |
+
+So the command sequence is **predict → units → grade → deliberate → delta → judge → decide** on
+pass 1, and **predict → deliberate → delta → judge → decide** on every pass after.
+
+## Commands
+
+### `ponder start "" --lens `
+
+```sh
+ponder start "the UI" --lens ui --max-passes 6 --min-passes 2
+ponder start "our error handling" --lens custom --rubric ./rubrics/errors.md --slug errors-aug
+```
+
+| Flag | Default | Meaning |
+|---|---|---|
+| `--lens ` | required | `ui`, `architecture`, `docs`, `tests`, `product`, `copy`, `custom` |
+| `--rubric ` | - | required with `--lens custom`; any file with the lens front-matter shape |
+| `--project ` | resolved (below) | The root every relative path in the session resolves against, and where `.ponder/` is rooted |
+| `--slug ` | `-` | Session id. Derived from the *subject*, not the lens, with leading stopwords dropped: `"the UI"` → `ui-2026-08-24`, `"our error handling"` → `error-handling-2026-08-24`. A collision gets a `-2` suffix; an explicit `--slug` that already exists exits `1` |
+| `--max-passes N` | 6 | Hitting it produces a `stop` with reason `budget`, never `converged` |
+| `--min-passes N` | 2 | Below it, `judge` always advises `continue` |
+
+Ponder has one mode. It deliberates and produces a plan; `ponder apply` is a separate, explicit step
+that emits an ordered work plan and still never edits your source.
+
+**The project root, and why the `project` line is worth reading.** Every relative path in the
+session - every unit `path`, every finding's `evidence` anchor, every phantom claim checked against
+one - resolves from `session.project`. Without `--project`, the engine walks up from the working
+directory: first for a project marker (`.git`, `.hg`, `.svn`, `package.json`, `go.mod`,
+`Cargo.toml`, `pyproject.toml`, `deno.json(c)`, `Gemfile`, `pom.xml`, `build.gradle(.kts)`,
+`composer.json`, `mix.exs`, `*.xcodeproj`, `*.xcworkspace`) - the nearest hit is the root, full
+stop - and only if there is no marker anywhere above you, again for an existing `.ponder` to adopt.
+Failing both, the working directory. `.ponder` never competes with a marker: it is where sessions
+are kept, not what makes something a project.
+
+`start` prints the answer and what decided it, and `session.json` records it in `projectFrom`
+(`how`: `explicit` | `marker` | `ponder` | `cwd`):
+
+```
+ project /abs/path/app (cwd is /abs/path/app/src; matched .git)
+ store /abs/path/app/.ponder/ui-2026-08-24
+```
+
+A root from `--project` or a marker is **established**; one adopted from a `.ponder` or fallen back
+to is **guessed**, and `start` and `status` warn when it is. A guessed root cannot certify an
+absence, so a phantom claim under one is refused (see `ponder delta`). If the printed root is not
+where the software lives, start again with `--project ` before doing the work.
+
+Creates the session and prints **Prior pondering** - up to five earlier ponder episodes on a similar
+subject, read from the OCA's `episodic_memory`. Read them; that is where the last session left off.
+No OCA, no section, no problem.
+
+### `ponder predict --churn <0-1> --claim ""`
+
+```sh
+ponder predict --churn 0.35 --claim "Pass 1 over-graded polish and never opened the error paths."
+```
+
+Written before you look at anything. `--churn` is your estimate of this pass's `churn` metric
+(deltas ÷ live findings), `--claim` is one line about what the pass will find. Scored at `judge` as
+`surprise = |predicted - actual|`. Sandbagging the estimate destroys the only signal that says
+whether you model the subject yet.
+
+**`--churn` is validated to 0 ≤ n ≤ 1 and exits `1` outside it.** On pass 1 the number does not
+matter much: there is no previous pass, so the engine reports no churn and scores no surprise. Write
+the honest guess anyway - `--claim` is the part that carries, and the pair is recorded in
+`passes/01/predict.json` and in `report.md`. The scoring starts at pass 2, where `churn` exists and
+`surprise` is the distance from it. `--claim` is required and must be non-empty - an unstated
+expectation cannot be scored.
+
+### `ponder units --file ` / `--stdin`
+
+Pass 1 only. Later passes amend the inventory with `unit-add`, `unit-edit` and `unit-delete` deltas -
+add what the survey missed, correct what it got wrong, remove what is out of scope.
+
+The inventory is written to the ledger too, as a single `unit-seed` record carrying every unit id in
+order and every unit body as stored. That is what makes the coverage denominator auditable: an
+unledgered inventory is a denominator taken on trust. `unit-seed` is a seed, not a delta - it is
+excluded from every convergence ratio and from the delta counts, exactly like `grade` seeding.
+
+### `ponder grade --file ` / `--stdin`
+
+Pass 1 only. Seeds the finding set. Every seeded finding is written to the ledger as an `add` with
+`source: "grade"`, so the ledger holds every creation - the thrash detector reads those creation
+values as the first setting of each field. They are excluded from every ratio, though: a seed is
+something the pass created, not something it changed, and counting it as movement is what used to
+pin pass 1's numbers near the ceiling. `metrics.json` reports the count separately as
+`deltas.seeded`. Grade-sourced adds are also excluded from thrash's killed-add rule, so pass 2
+deleting pass 1's noise reads as the Skeptic working, not as the loop oscillating. A seeded finding
+may carry its own `why`, which becomes that ledger entry's reason.
+
+### `ponder deliberate --file ` / `--stdin`
+
+```sh
+ponder deliberate --file .ponder/tmp/deliberation-2.json
+ponder deliberate --skip --why "Pass 4 was a two-unit re-check; nothing to argue about."
+```
+
+`--skip` is legal and is recorded as a `deliberation_skipped` metacognitive observation against the
+session. It shows up in `report.md`.
+
+### `ponder delta --file ` / `--stdin`
+
+Applies the pass's revisions in file order, **all-or-nothing**: one bad line on line 40 means nothing
+in the batch is applied, and every problem is listed at once with its line number. Every delta needs a
+non-empty `why` and a `perspective`. A `unit-add` must sit above any `add` that names the unit it
+creates, and above any `unit-edit` or `unit-delete` that targets it - the inventory is built as the
+file is read, so the reverse order fails with `unknown unit "…"`.
+
+`--file` also accepts a JSON array of delta objects, not only JSONL. Blank lines are ignored.
+
+### `ponder judge`
+
+Computes the metrics for the current pass, writes `passes/NN/metrics.json`, folds in any open OCA
+metacognitive observations as a tie-breaker toward `escalate`, and prints advice. Advice only -
+nothing is committed until `decide`.
+
+### `ponder decide --why ""`
+
+Commits your call, closes the pass, and opens the next one (or closes the session on `stop`).
+
+- Every decision needs a `--why`, override or not. It is written to `verdict.json` and, for
+ overrides, surfaced permanently in `report.md` under **Overrides**.
+- You may override the advice, **except**: `stop` exits `2` and prints the ungraded units whenever
+ coverage is below 1.0. There is no flag for it. Note what the gate actually reads - not the verdict
+ label on disk, but coverage recomputed from `units.json` and `findings.json` at decision time, so
+ a stale or hand-edited `metrics.json` / `verdict.json` changes nothing. `escalate` while blocked is
+ legal and warns that coverage still has to reach 1.0 before the session can stop.
+- Overriding requires at least 12 characters of `--why`.
+- `stop` while `voices < 3` warns and proceeds, naming which perspectives actually spoke in the
+ pass's deliberation. A pass that skipped deliberation heard nobody, so it always warns.
+- Only a `stop` on reason `converged` closes the session as `converged`; every other stop -
+ `budget`, `perseveration`, an override - closes it as `stopped`, and `report.md` says which.
+
+### `ponder status`
+
+Where you are and what you owe: session identity, pass number, phase, unit and finding counts, last
+verdict + decision + reason, the project root and what decided it, and the ungraded unit list when
+there is one. `--json` for the same thing parseable. **This is the resumption command.** Run it
+before concluding anything about a session you did not personally start ten minutes ago - and read
+the `project` line while you are there: a session whose root was guessed has been resolving every
+path in it against a directory nobody chose, and it says so.
+
+### `ponder verify [--repair]`
+
+Replays `ledger.jsonl` from line 1, derives the state those records must have produced, and compares
+it against what `units.json` and `findings.json` actually contain - then compares the ledger against
+the per-pass `deltas.jsonl` files, which are the same records written a second time. Exit `0` and a
+summary when the session reconciles; exit `2` and the full diagnosis when it does not.
+
+```
+ponder verify - ui-2026-08-24
+ ledger 41 records - 1 unit-seed, 24 add, 1 unit-add, 2 unit-edit, 1 unit-delete
+ passes 41 of those records also in the per-pass delta files, across 4 pass directories
+ units 12 in units.json, 12 derived from the ledger
+ findings 29 in findings.json, 29 derived from the ledger
+ result reconciles - every unit and every finding is accounted for by a ledger record
+```
+
+**Run it first on any session you did not personally start**, and after any interruption. Without
+`--repair` it never writes anything, and it takes no lock - it is the one command whose locking
+depends on a flag. Full mechanics - what is reconciled, what `--repair` rebuilds and what it refuses
+- are under [The ledger is the authority](#the-ledger-is-the-authority) below.
+
+`--json` carries `divergences`, `ledgerProblems`, `unionProblems`, `ledgerSkipped`, `repaired`,
+`rebuilt`, `unrepairable` and `counts`, and a failure returns the usual
+`{ "ok": false, "error", "detail", "code": 2, "next" }`.
+
+### `ponder report [--out ]`
+
+Regenerates the readable summary from current state, in this order: **Convergence**, **Overrides**,
+**Prediction vs reality**, **Coverage** (with **Corrected \** and **Phantom units** under
+it), **Findings** grouped by unit, **Retracted**, **Deliberation** pass by pass, and the full
+**Ledger**. Safe to run mid-loop.
+`--out` redirects it: the report is written to that path **instead of** `.ponder//report.md`,
+not as well as, so run `ponder report` on its own first if you want the session's own copy refreshed
+too. Either way the command prints where it wrote.
+
+### `ponder apply [--dry-run] [--severity must,should]`
+
+Separate and explicit; never part of a pass. Selects `status: open` findings at or above the given
+severity (default `must`) and emits an ordered work plan grouped by unit, with evidence paths and
+recommendations. `--dry-run` prints it and touches nothing. Without `--dry-run` it prints it, writes
+`apply-plan.md`, marks the session `applied` - a terminal status - and records an OCA episodic entry.
+**It refuses (exit `2`) without `--dry-run` while the session is still open**, so it cannot close a
+pondering behind your back, and **it refuses (exit `2`) either way on an aborted session**
+(`session was aborted; there is no plan to apply.`) - a pondering abandoned mid-loop has
+findings nobody finished arguing with, and must not be able to quietly become an applied one. **Ponder never edits the subject's source.** It hands you a plan; you do the work.
+
+### `ponder list` / `ponder abort --why ""` / `ponder doctor` / `ponder version` / `ponder help`
+
+`list` shows every session both stores can see from here - this project's `.ponder/` and the
+`~/.ponder/` fallback - with slug, status, pass, phase, coverage, lens and subject, and prints the
+store roots it searched. It takes no arguments; use it to recover a slug you have forgotten. `abort` closes a
+session as `aborted` with a permanent reason - use it when the subject changed under you, rather
+than silently re-scoping an open session. `doctor` prints node version, `psql` presence, the DSN it
+would use, whether the OCA is reachable, where state will be written, the project root it resolves
+from here and what decided it, **and whether every session it can see reconciles with its own
+ledger** - a session that does not is named under **Problems** with the
+`ponder verify --session ` that diagnoses it. A project root adopted from an ancestor
+`.ponder` rather than established by a project marker is a **Problem** too, in this directory or in
+any session it can see: that is the configuration in which a path that fails to resolve means
+nothing, and it is worth knowing before you trust one. Run it first in an unfamiliar harness.
+
+`ponder version` - or `ponder --version` - prints the engine version (`ponder 1.0.0`) and nothing
+else; `--json` wraps it in the usual envelope as `version`. `ponder help` lists
+every command with its usage line; `ponder help ` prints one, and `--help` works on any
+command as a flag. Everything the engine can do is discoverable from those two - a command that runs
+but is documented nowhere is a command nobody can find.
+
+## File shapes
+
+### `units.json`
+
+```json
+{
+ "version": 1,
+ "lens": "ui",
+ "units": [
+ { "id": "screen.settings.billing",
+ "label": "Settings → Billing",
+ "path": "src/routes/settings/billing.tsx",
+ "kind": "screen",
+ "meta": { "route": "/settings/billing" } }
+ ]
+}
+```
+
+`id` is required, unique, lowercase dotted slug, and **the one field nothing can change** - it is the
+join key for findings, so moving it would orphan every finding hanging off that unit. A `unit-edit`
+that tries is rejected, exit `1`. `path` is optional but strongly preferred: it is the evidence
+anchor, and it is what lets the engine check a phantom claim. `addedInPass` is stamped by the engine.
+
+`label`, `path`, `kind` and `meta` are all correctable by a later pass with a `unit-edit` delta - see
+below. A unit that has left the inventory also carries `removedInPass`, `removalKind`
+(`appraised` | `removed-ungraded` | `phantom`), and on a phantom `phantomVerification` and
+`phantomPath`. All of those are engine-stamped and all of them are reconciled against the ledger.
+
+### `findings.json` (what `ponder grade` takes)
+
+```json
+{
+ "version": 1,
+ "findings": [
+ { "unit": "screen.settings.billing",
+ "title": "Empty state is a bare 'No invoices' string",
+ "detail": "No illustration, no explanation of when invoices appear, no primary action.",
+ "dimension": "empty-states",
+ "score": 2,
+ "severity": "must",
+ "confidence": 0.8,
+ "effort": "s",
+ "evidence": ["src/routes/settings/billing.tsx:88-94"],
+ "recommendation": "Add an empty state with cause, expectation and a primary action." }
+ ]
+}
+```
+
+| Field | Values | Notes |
+|---|---|---|
+| `unit` | a unit `id` | Must exist in the inventory |
+| `dimension` | one of the lens's dimensions | Drives duplicate detection and reversal detection |
+| `score` | integer 1-5 | 1 broken, 3 acceptable, 5 exemplary; the lens defines each rung |
+| `severity` | `must` \| `should` \| `could` \| `note` | |
+| `confidence` | 0-1 | Below 0.5 it is a hypothesis, not a conclusion |
+| `effort` | `xs` \| `s` \| `m` \| `l` \| `xl` | |
+| `evidence` | array of `path:line-line` | The thing that makes a finding arguable instead of an opinion |
+| `status` | `open` \| `resolved` \| `retracted` \| `deferred` | Engine-managed; starts `open` |
+
+The engine assigns `id` (`f-NNNN`, never reused), `status`, `createdInPass`, `lastTouchedPass` and
+`history`. Do not send them.
+
+### `deliberation.json`
+
+```json
+{
+ "question": "Does the pass-1 finding set survive a second reading of the code?",
+ "skeptic": { "argument": "…", "confidence": 0.85 },
+ "builder": { "argument": "…", "confidence": 0.7 },
+ "dreamer": { "argument": "…", "confidence": 0.75 },
+ "empath": { "argument": "…", "confidence": 0.7 }
+}
+```
+
+The four keys and their `argument` / `confidence` map one-to-one onto the OCA `deliberations`
+columns (`skeptic_argument`, `skeptic_confidence`, …), which is why this is the shape to write.
+`argument` must be a non-empty string; `confidence` is optional and must be 0-1 if present. Fewer
+than four keys is accepted, and `deliberate` warns which voices were silent - those are exactly the
+ones missing from this pass's `voices`, so a two-key file is a pass that will warn again at
+`decide stop`.
+
+Two other shapes are tolerated and mean the same thing - `{"perspectives": {"skeptic": {…}, …}}`
+and an array of `{"perspective": "skeptic", "argument": …}` entries. The engine's own error message
+advertises the `perspectives` wrapper, so do not be surprised by it; the flat form above is fine.
+
+`question` is optional. It is stored in `passes/NN/deliberation.json` and goes no further - the
+`deliberations` row's `decision` column is a string the bridge synthesises (`ponder pass N:
+revise the finding set for ""`), not your question. The pass's delta summary becomes
+`resolution`, with `resolution_method = 'ponder-pass'` - in ponder, the executive-control synthesis
+*is* the revise phase.
+
+### `deltas.jsonl` and `ledger.jsonl`
+
+One JSON object per line. You supply `op`, `perspective`, `why`, and the op's payload; the engine
+stamps `pass` and `at`.
+
+```
+{"op":"delete","target":"f-0012","perspective":"skeptic","why":"Duplicate of f-0004."}
+{"op":"add","perspective":"dreamer","why":"No pass had looked at loading states as a set.","finding":{"unit":"screen.dashboard","title":"…","detail":"…","dimension":"loading-states","score":2,"severity":"should","confidence":0.7,"effort":"s","evidence":["src/routes/dashboard.tsx:20-31"],"recommendation":"…"}}
+{"op":"edit","target":"f-0007","perspective":"builder","why":"Recommendation was not actionable.","set":{"recommendation":"…"}}
+{"op":"change","target":"f-0009","perspective":"empath","why":"Blocks first-run, outranks polish.","set":{"severity":"must","score":2}}
+{"op":"unit-add","perspective":"dreamer","why":"Route was missed in pass 1.","unit":{"id":"screen.invite.accept","label":"Accept invite","path":"src/routes/invite/accept.tsx","kind":"screen","meta":{"route":"/invite/:token"}}}
+{"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":"/"}}}
+{"op":"unit-delete","target":"screen.legacy.print","perspective":"skeptic","why":"Route was removed in #1841; the file is dead.","phantom":true}
+```
+
+Seven ops. Four revise the finding set, three amend the inventory.
+
+| Op | Carries | Owner | For |
+|---|---|---|---|
+| `add` | `finding` (a whole finding object, no `id`) | Dreamer | Ground nobody had covered |
+| `edit` | `set` (partial finding) | Builder | Prose and evidence - `title`, `detail`, `recommendation`, `evidence` |
+| `change` | `set` (partial finding) | Empath | Grades - `score`, `severity`, `confidence`, `effort`, `status` |
+| `delete` | `target` | Skeptic | Unsupported, duplicated, or wrong. Sets `status: "retracted"` |
+| `unit-add` | `unit` | any | Inventory amendment. **Drops coverage below 1.0 until the unit is graded** |
+| `unit-edit` | `target` (a unit id) + `set` (partial unit) | any | Correcting a unit in place - `label`, `path`, `kind`, `meta`. **Never touches coverage** - see below |
+| `unit-delete` | `target` (a unit id), optionally `phantom` | any | Inventory amendment. What it costs coverage depends entirely on whether the unit was ever graded - see below |
+
+The engine does not enforce which fields go with which op, but the conformance suite warns when a
+`change` edits only prose - that hides re-grading from the thrash detector. `delete` is for findings
+that are wrong, not findings that are unimportant; unimportant is a `change` to `severity`, or
+`status: "deferred"`.
+
+### `unit-edit` - correcting a unit in place
+
+A unit's `label` is a caption on the inventory, and captions go stale. Pass 1 writes
+`"Billing - both links 404"`; pass 3 opens the file, finds one of them resolves, and rewrites the
+finding underneath. Without `unit-edit` the caption keeps saying 404 for ever, and `report.md` ships a
+headline contradicting the finding it sits above.
+
+```json
+{"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":"/"}}}
+```
+
+(Wrapped here to read. In the file it is one object on one line, like every other delta.)
+
+| Field | Rule |
+|---|---|
+| `target` | a **live** unit id. Unknown unit → rejected. A unit already removed → rejected: *"a unit that has left the inventory is not edited back into shape, it is re-added"* |
+| `set` | non-empty object. May set `label`, `path`, `kind`, `meta` - and nothing else. An unknown field is rejected by name: `"note" is not a settable unit field (label, path, kind, meta)` |
+| `set.id` | **always rejected, exit `1`**, by name rather than as an unknown field. The id is the join key between `units.json` and `findings.json`; moving it orphans every finding on that unit. If the id itself is wrong, `unit-delete` it and `unit-add` the corrected one, and accept what that costs coverage |
+| `why` | required and non-empty, like every delta |
+
+Value rules: `label` must be a non-empty string. `path` and `kind` take a non-empty string or `null`
+(clearing a path clears the anchor, not the *history* of the anchor - see the phantom rule below).
+`meta` must be an object.
+
+**`meta` merges key by key.** Every other field replaces wholesale. Correcting
+`{"route":"/settings/billing"}` on a unit whose meta is `{"route":"/billing","state":"empty"}` leaves
+`state` sitting exactly where it was - fixing a route must not silently wipe the state marker beside
+it. Replay applies the same merge, so the ledger derives byte-identical state.
+
+What it costs:
+
+- **Coverage: nothing.** A `unit-edit` is neither an addition nor a removal, so the numerator and the
+ denominator are both exactly what they were. This is the whole reason it exists: correcting a
+ caption with `unit-delete` + `unit-add` would drop coverage below 1.0 and block the stop, which
+ turns a wrong caption into a reopened session.
+- **Churn: yes.** It is a revising delta. It goes on the ledger, it counts toward `churn`, and it
+ participates in thrash detection - a label moved in pass 2 and put back in pass 3 is a
+ `field-revert` exactly like a severity flipped back and forth. Reversal is judged on the
+ *effective* value, so two edits touching different `meta` keys are not a false reversal.
+- **The record: permanent.** Every `unit-edit` appears in `report.md` under **Corrected \**
+ with its pass, its perspective, each field's new value and its `why` - the same treatment a removal
+ gets. A hand-edited label in `units.json` with no ledger record behind it is a divergence, and
+ every decision-point command refuses the session until it reconciles - which `verify --repair`
+ does by putting the ledger's value back. The hand edit loses; that is the design.
+
+### `unit-delete` and the phantom rule
+
+`unit-delete` is the only op that can shorten the ungraded list, which makes it the only op with an
+obvious route past the coverage gate: enumerate twelve screens, look at eleven, delete the twelfth.
+The engine closes that route. Three outcomes, decided by the engine, not by you:
+
+| The unit | `phantom` | What happens |
+|---|---|---|
+| carries at least one finding (a retracted one counts - withdrawing a judgment does not un-see the thing) | omitted | **`appraised`** removal. An honest scope change: it leaves the inventory *and* the coverage denominator |
+| was never graded | omitted | **`removed-ungraded`**. It leaves the inventory but **stays in the coverage denominator**, still listed as ungraded. Coverage does not move and the gate does not open |
+| was never graded | `true` | **`phantom`**, once the engine has checked the claim. Then it leaves the denominator |
+
+A `phantom` claim is checked against **every path the unit has ever carried**, not just the one
+`units.json` holds now: the original from its `unit-seed` or `unit-add` ledger record, plus every
+value any `unit-edit` has set since, plus the current one - resolved from the project root,
+deduplicated, and including a path set earlier in the *same* delta batch.
+
+That is not belt and braces. `unit-edit` can move a `path`, so a claim checked only against the
+current value would be a two-delta route through the coverage gate: point a real unit at a filename
+that does not exist, then claim the unit never existed and let the engine certify the absence. One
+surviving path sinks the claim.
+
+| The unit's paths | Result |
+|---|---|
+| **any** of them exists | **the whole batch is rejected, exit `1`.** The message names the path found and how it got there: `unit "" is claimed phantom, but a path it has carried exists: (set by a unit-edit in pass 3). 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.` |
+| exactly one, never edited, and it does not exist | accepted · `verified-missing`, with the checked absolute path recorded |
+| the path was ever edited and no version of it exists | accepted · **`unverified`, never `verified-missing`.** Every version was checked and every one was missing, but the engine will not certify an absence about a target that moved. The ledger record carries `pathsChecked`, and `ponder delta` and `report.md` both say so |
+| the unit never had a path | accepted · `unverified` - nothing could be checked at all |
+
+Those rows describe a root the engine is sure of: `--project`, or one a project marker established.
+Under a **guessed** root - an adopted ancestor `.ponder`, or a working-directory fallback (see
+`ponder start`) - a unit with paths and no path that resolves is refused, exit `1`, naming the root
+and `--project`: `none of the 2 paths it has carried … resolves under - but that project root
+was guessed, not established`. Nothing was really checked, and a file missing from the wrong
+directory is not a screen that never existed. A unit that never had a path is unaffected.
+
+So editing a path is never a way to *strengthen* a phantom claim. At best it leaves the claim exactly
+where it would have been with no path at all; at worst it is refused outright.
+
+Also refused: `"phantom": true` on a unit that carries a finding (`it cannot also be a unit that never
+existed`), `phantom` on any op other than `unit-delete`, a `phantom` that is not a boolean, and a
+second `unit-delete` on a unit already removed - with exactly one exception, which is upgrading a
+`removed-ungraded` unit to a checked phantom claim once you have gone and looked.
+
+Every accepted phantom is named permanently in `report.md` under **Phantom units**, with its
+verification state and your `why` quoted underneath - and the two unverified cases are written
+differently there, because they are different claims: *"this unit had no path, so nothing could be
+checked"* versus *"this unit's path was edited, so every version of it was checked and none was
+found, but the engine will not certify an absence about a target that moved."* A unit deleted while
+ungraded is named too, in the **Ungraded** list it never left. That is the deal: you may narrow the
+subject, and the narrowing is on the record for ever.
+
+**`phantom` means the route does not exist. It does not mean you ran out of patience.** The engine can
+only catch the lie when there is a path to check; the unverified case runs on your word.
+
+### `metrics.json`, `verdict.json` and `judge --json`
+
+`ponder judge --json` prints one envelope. Every command's `--json` has the same outer shape -
+`ok`, `command`, `session`, the command's own payload, `warnings`, `next` - and nothing else reaches
+stdout, including warnings, which move into the `warnings` array:
+
+```json
+{ "ok": true, "command": "judge", "session": "ui-2026-08-24",
+ "metrics": { "…": "the metrics.json object, inline" },
+ "verdict": "blocked", "reason": "coverage",
+ "message": "1 of 12 screen has never been graded. Stopping is not permitted.",
+ "ungraded": ["screen.invite.accept"],
+ "observations": [],
+ "warnings": [],
+ "next": { "phase": "decide", "command": "ponder decide continue --why \"\" …" } }
+```
+
+On failure the envelope is `{ "ok": false, "error", "detail", "code", "next" }` and the process exits
+with `code`.
+
+`metrics.json` keys: `version`, `pass`, `at`, the eight signals (`coverage`, `novelty`, `churn`,
+`thrash`, `surprise`, `stall`, `confidenceFloor`, `voices`), plus `voiceNames`,
+`deliberationSkipped`, `deltaVoices`, `deltaVoiceNames`, `predictedChurn`, `claim`, `units`
+(`enumerated`, `graded`, `removed`, `removedUngraded`, `phantom`), `ungraded`, `removedUngraded`,
+`phantoms`, `findings` counts, `deltas` (`total`, `byOp`, `seeded`, `revisions`), `reversals` (a note
+per reversing delta), `shakyMustFindings`, `minPasses`, `maxPasses`. Numbers are rounded to four
+places, so the table `judge` prints (two places) and the file disagree in the last digit - trust the
+file. On pass 1, `novelty`, `churn`, `thrash` and `surprise` are `null`, not `0`.
+
+`verdict.json` keys: `version`, `pass`, `verdict`, `reason`, `rule`, `message`, `notes`, `ungraded`,
+`signals` (the eight, copied, plus `deltaVoices`), `removedUngraded`, `phantoms`, `advisedAt`, then
+after `decide`: `decision`, `why`, `override`, `warnings`, `decidedAt`, `coverageAtDecision`.
+
+`reason` is a stable label, not prose - one of `coverage`, `min-passes`, `thrash`, `perseveration`,
+`low-confidence`, `converged`, `budget`, `oca-observations`, `default`. The human sentence is in
+`message`.
+
+## The ledger is the authority
+
+Everything above governs what the *engine* will do. None of it governs a text editor. `units.json` and
+`findings.json` are ordinary files: forge a `removalKind`, delete a unit's entry, invent a finding,
+flip a status from `open` to `resolved`. So the state is not trusted, it is **recomputed**.
+
+**Reconciliation** replays `ledger.jsonl` from line 1, derives the state those records must have
+produced, and compares:
+
+| Document | Reconciled |
+|---|---|
+| `findings.json` | which ids exist; and per finding `unit`, `title`, `detail`, `dimension`, `score`, `severity`, `confidence`, `effort`, `evidence`, `recommendation`, `status`, `createdInPass` |
+| `units.json` | which ids exist; and per unit `label`, `path`, `kind`, `meta`, `addedInPass`, `removedInPass`, `removalKind`, `phantomVerification`, `phantomPath` |
+
+`label`, `path`, `kind` and `meta` are in that list *because* `unit-edit` replays them, key-by-key
+`meta` merge included. Correcting a caption on the record is cheap; correcting one off the record is
+not possible. Not reconciled: `lastTouchedPass`, a finding's `history`, timestamps, and `session.json`
+- derived bookkeeping and the phase machine, neither of which can buy anyone coverage.
+
+It then checks the ledger against itself. `ledger.jsonl` is the ordered union of the per-pass
+`deltas.jsonl` files - one call writes both copies - so those files are an independent second copy of
+every record. That is what catches a plausible line appended to `ledger.jsonl` alone, a reordered
+ledger, a truncated one, or a pass directory deleted out from under it.
+
+**`judge`, `decide`, `report` and `apply` reconcile before they act**, and a divergence is exit `2`
+naming what diverged. Those four turn state into a claim - a verdict, a decision, a document, a work
+plan. The rest of the loop does not pay the cost: `predict`, `units`, `grade`, `delta`, `status` and
+`list` run without it.
+
+### Write ordering - ledger first, always
+
+Every command that changes state appends to `ledger.jsonl` **first** and writes `units.json`,
+`findings.json` and `passes/NN/deltas.jsonl` **after**. The asymmetry is deliberate:
+
+- **Ledger ahead** - fully recoverable. Every record determines what the state files must contain, so
+ replaying derives what they have not caught up to. Deterministic, no guessing.
+- **State ahead** - unrecoverable. The extra data exists in exactly one place and there is nothing to
+ rebuild it from.
+
+So the only crash state reachable is the recoverable one. A torn write never costs a pass's work.
+
+### `ponder verify --repair`
+
+Repairs a ledger-ahead session: replays the ledger, rewrites `units.json` and `findings.json` to the
+state those records must have produced, appends the missing tail to any `passes/NN/deltas.jsonl` that
+holds a strict prefix, then re-reconciles and prints line by line what it rebuilt.
+
+```
+Repaired from the ledger
+ - units.json - 12 units rebuilt from the ledger (screen.auth.sso, screen.billing were behind it)
+ - passes/03/deltas.jsonl - 2 records appended to catch it up to the ledger
+```
+
+| What `--repair` found | What it does |
+|---|---|
+| a unit or finding the ledger created that the state file lacks, or a field whose stored value is behind the ledger's | rebuilds it |
+| a `passes/NN/deltas.jsonl` holding a strict prefix of the ledger's records for that pass | appends the missing tail |
+| a unit or finding in a state file that **no ledger record ever created** | **refuses** - the data exists in no other copy. Forgery or unrecoverable loss; both are a hard error |
+| a duplicated or unidentified entry in a state file | **refuses** - same reason |
+| a `passes/NN/deltas.jsonl` holding a record the ledger does not, or a different record at the same position | **refuses** - two copies of an append-only log disagreeing, not one lagging |
+| a problem inside `ledger.jsonl` itself, or a line that will not parse | **refuses** - repair rebuilds *from* the ledger, so a broken ledger is nothing to rebuild from |
+
+Every refusal stays exit `2`, and `--repair` prints what it cannot rebuild before it refuses. A
+repaired session is byte-identical to what the interrupted command would have written. Without
+`--repair`, `verify` writes nothing.
+
+**What this does not guarantee.** It is not a signature and not a hash chain. Someone who edits all
+three copies consistently - the state file, `ledger.jsonl`, and the matching `passes/NN/deltas.jsonl`
+at the right position - defeats it, and that is outside the threat model, said plainly rather than
+implied away. What it does is raise the cost of a dishonest stop from one edit to a consistent forgery
+across three files, one of them an append-only log that `report.md` quotes.
+
+Two boundaries worth knowing before you meet them. `session.json` is not reconciled and `--repair`
+does not rebuild it - the ledger records operations on units and findings, not the phase machine. And
+a session whose inventory was seeded before `unit-seed` existed has no ledger record for its units,
+so it will never reconcile: re-seed it rather than working around it.
+
+## Convergence signals
+
+| Signal | Formula | Reads as |
+|---|---|---|
+| `coverage` | graded units ÷ enumerated units | Did we actually look at everything? |
+| `novelty` | revising `add` deltas ÷ live findings - **null on pass 1** | Still finding new ground? |
+| `churn` | revising deltas ÷ live findings - **null on pass 1** | How much moved this pass? |
+| `thrash` | revising deltas that reverse an earlier delta on the same target ÷ revising deltas - **null on pass 1** | Oscillating instead of converging? |
+| `surprise` | \|predicted churn - actual churn\| - **null on pass 1** | How well the loop models itself |
+| `stall` | consecutive passes with `churn < 0.02` | Perseveration guard |
+| `confidenceFloor` | fraction of `open` + `must` findings with `confidence < 0.5` | Are the important calls still guesses? |
+| `voices` | distinct perspectives that **spoke in this pass's deliberation** | Did the pass argue with itself? |
+
+**Pass 1 reports `null` for `novelty`, `churn`, `thrash` and `surprise`** - printed as `-` in the
+signal table, and `-` is not a zero. All four measure change against a previous pass, and pass 1 has
+none: every finding is new because there was nothing there before, and there is no actual churn to
+score the prediction against. They start reading at pass 2. `coverage`, `confidenceFloor`, `stall` and
+`voices` describe a state rather than a change, so they read from pass 1 onward.
+
+The numerator for `novelty` and `churn` is **revising** deltas - the seed `add`s that `ponder grade`
+writes are excluded, so the numbers describe what a pass *changed*, not what it created. The
+denominator is the **live** finding count, total minus retracted, so retracting noise does not dilute
+the next pass's signal. `confidenceFloor`'s denominator is only the `open` + `must` findings.
+
+`thrash` is the odd one out: **both** halves of it are revising deltas, not total deltas, so it reads
+as the share of this pass's own revisions that undid earlier work. A `unit-add`, `unit-edit` or
+`unit-delete` is a revising delta like any other - inventory amendments count in `churn`, because they
+moved the pondering, and never in `novelty`, because they are not `add`s of findings.
+
+**Coverage is the one signal a `unit-edit` cannot move.** Correcting a unit is neither an addition nor
+a removal, so the numerator and the denominator come out of the pass exactly as they went in.
+
+`voices` counts the perspectives that **argued**, read from `passes/NN/deliberation.json`: a
+perspective with a non-empty `argument` spoke, whatever it did or did not go on to emit. A Skeptic
+who checked the evidence, found the finding sound and said so has voted - the deliberation heard it.
+A pass that skipped deliberation (`ponder deliberate --skip`) heard nobody, and its `voices` is 0.
+
+The old reading is still available and still worth watching: `judge` prints a **`delta voices`** row
+under `voices`, and `metrics.json` carries `deltaVoices` / `deltaVoiceNames`. Read the two together.
+`voices 4/4` with `delta voices 1/4` is four perspectives arguing and one acting - sometimes a genuine
+quiet pass, sometimes three voices going through the motions. `voices 0/4` with a healthy
+`delta voices` is deltas nobody argued for.
+
+A delta **reverses** an earlier one when, on the same target, an earlier pass set a field to X, a
+later pass changed it, and this pass sets it back to X - on a unit's `label`, `path`, `kind` or `meta`
+via `unit-edit` exactly as on a finding's fields, with seeded and added values counting as the first
+setting and `meta` compared on its merged value rather than on the partial object submitted; or when
+this pass `add`s a finding materially matching one an earlier pass `delete`d (same `unit` + same
+`dimension`); or when it `delete`s one added within the previous two passes.
+
+## Verdict rules, in order
+
+First match wins.
+
+1. `coverage < 1.0` → **`blocked`**. Stopping is not permitted; the verdict names every ungraded
+ unit, and names apart the ones deleted without ever being graded, saying why they still count.
+ No override exists.
+2. `pass < minPasses` → **`continue`**. A single pass is an opinion, not a pondering.
+3. `thrash > 0.25` → **`escalate`**. Another identical pass will not help - rotate perspectives,
+ widen the evidence, or split the subject. Skipped when `thrash` is null.
+4. **Evidence and review guard (`confidenceFloor > 0` or `voices < 3`):** any open `must` finding
+ below 0.5 confidence (`confidenceFloor > 0`) or fewer than three reviewing perspectives
+ (`voices < 3`) blocks convergence → if `pass >= maxPasses`, **`stop`** with reason `budget`;
+ else if `stall >= 2`, **`escalate`**; else **`continue`** with reason `low-confidence` or
+ `insufficient-perspectives`. Stillness with unresolved uncertainty does not earn convergence,
+ and no quiet pass bypasses this guard.
+5. `novelty < 0.05` and `churn < 0.10`, with `voices >= 3` and `confidenceFloor == 0`, sustained
+ two consecutive passes → **`stop`**, reason `converged` (engine rule id `6`). Checked before
+ the fallback perseveration stop so two fully reviewed, confident quiet passes earn `converged`.
+6. `stall >= 2` and `coverage == 1.0` → **`stop`**, reason `perseveration` (engine rule id `4`).
+ Nothing is moving, and the two-pass convergence check did not match (for example, because the
+ prior pass lacked full perspective review or confidence). `stall` only counts passes whose churn
+ was a number, so a pass with no churn cannot feed it.
+7. `pass >= maxPasses` → **`stop`**, reason `budget`. Report it as a budget stop, never as
+ convergence.
+8. otherwise → **`continue`**, reason `default` - unless the OCA is holding open metacognitive
+ observations of high severity about this loop, which tip an otherwise-`continue` into
+ **`escalate`**, reason `oca-observations`.
+
+Pass 1 reports no `churn`, `novelty` or `thrash`, so rules 3, 5 and 6 have nothing to fire on there,
+and the skip is explicit rather than a null comparison happening to come out false. Rule 1 still
+applies in full - coverage is computed from the inventory and the finding set, not from the ledger,
+and it blocks pass 1 exactly as hard as pass 6.
+
+## Lens packs
+
+`lenses/.md`, read by the agent at survey and appraise:
+
+```yaml
+---
+name: ui
+unit: screen
+enumerate: |
+ How to find every unit. Concrete commands and heuristics.
+dimensions: [information-hierarchy, empty-states, loading-states, error-states,
+ motion, affordance-clarity, density, responsive-behaviour,
+ accessibility, copy-tone, consistency, delight]
+scale: { 1: broken, 2: bad, 3: acceptable, 4: good, 5: exemplary }
+---
+```
+
+The body defines what a unit is, how to enumerate exhaustively, each dimension with concrete 1 / 3 / 5
+examples, what counts as evidence, and what each of the four perspectives should hunt for.
+`--lens custom --rubric ` loads any file with the same front-matter shape.
+
+## OCA bridge
+
+Optional, silent when absent, zero npm dependencies - it shells out to `psql`.
+
+| Variable | Default | Meaning |
+|---|---|---|
+| `PONDER_OCA` | `auto` | `auto` probes once per process and disables itself on failure; `on` makes an unreachable OCA a hard error; `off` skips it entirely |
+| `PONDER_OCA_DSN` | `postgres://localhost/oneiro` | |
+
+Timeout is 5s per call. Any non-zero exit, timeout or parse failure is logged to
+`.ponder//oca.log` and returns `null`. **The bridge can never break the engine** - if a pass
+behaves strangely, the OCA is not the reason; check `oca.log` and move on.
+
+What it buys you: `start` prints prior pondering on the same subject; each pass writes a scored
+prediction to `episodic_memory` and `hypotheses`; deliberations land in `deliberations`; stuck,
+perseverating, thrashing and low-confidence passes raise `metacognitive_observations` that `judge`
+reads back as a tie-breaker toward `escalate`. The cognitive-loop process on `:3333` is **not**
+running and nothing here depends on it - the bridge talks to Postgres directly.
+
+## Common errors
+
+Messages below are the literal ones the engine prints. Errors go to stderr; the `NEXT:` line still
+goes to stdout, so a failed command still tells you where you are.
+
+| Exit | Message | Cause | Fix |
+|---|---|---|---|
+| `2` | `out of order: "ponder judge" satisfies judge, but this session owes decide.` | A phase skipped or repeated | Do what `NEXT:` says. The state machine is correcting your memory, not malfunctioning |
+| `2` | `cannot stop: coverage is 0.67, not 1.0. 1 screen has never been graded.` | The coverage gate | Grade the named units. A `unit-delete` does not shorten this list unless the unit was graded or is a verified phantom |
+| `2` | `session is still open on pass N ().` | `ponder apply` without `--dry-run` on an open session | `--dry-run` to preview, or finish the pondering first |
+| `2` | `session was aborted; there is no plan to apply.` | `ponder apply` on an aborted session, with or without `--dry-run` | Nothing to fix. An abandoned pondering is not a plan - start a fresh session |
+| `2` | `pass N has not been judged.` | The session owes `decide` but `passes/NN/metrics.json` is missing - the phase machine catches an ordinary `decide`-before-`judge` first, so this one means the pass artefacts were disturbed | `ponder judge` again |
+| `1` | `N problems in this delta batch - nothing was applied.` + `delta line 4: every delta needs a non-empty "why" - an unexplained revision is not a revision.` | A delta without a reason | Every delta carries a `why`. Nothing in the batch applied, so fix and re-run the whole file |
+| `1` | `delta line 1: finding targets unknown unit "u.two" - add it first with a unit-add delta.` | An `add` naming a unit that is not in the inventory *yet* | Move the `unit-add` line above the `add`. Same file is fine; order is not optional |
+| `1` | `unit "" is claimed phantom, but a path it has carried exists: (carried by the unit-add that created it in pass 4).` | A `"phantom": true` on a unit that has, at some point, pointed at a file you can open | Go and grade it. The file is right there. The check runs against every path the unit ever had, so moving the path first does not clear it |
+| `1` | `unit "" is claimed phantom and none of the 2 paths it has carried (…) resolves under - but that project root was guessed, not established (…).` | A `"phantom": true` under a project root no `--project` and no project marker established | Check the `project` line from `ponder start` / `ponder status`. If the root is wrong, start again with `--project `; if it is right, the claim is still uncheckable from there and the unit needs grading |
+| `1` | `unit-edit may never set "id". The unit id is the join key between units.json and findings.json…` | A `unit-edit` trying to rename a unit | Ids do not move. If the id itself is wrong, `unit-delete` it and `unit-add` the corrected one, and pay the coverage |
+| `1` | `"note" is not a settable unit field (label, path, kind, meta).` | A `unit-edit` setting something that is not one of the four | Those four, and nothing else |
+| `1` | `unit "" was removed in pass 3 - a unit that has left the inventory is not edited back into shape, it is re-added.` | A `unit-edit` on a removed unit | `unit-add` it back if it belongs in the subject |
+| `2` | `session does not reconcile with its ledger - 2 divergences.` | `judge`, `decide`, `report` or `apply` on state that disagrees with the ledger | `ponder verify --session ` for the diagnosis, then `ponder verify --repair` if the ledger is merely ahead. A state-ahead session is a hard error by design |
+| `1` | `--churn must be between 0 and 1 (got 1.2).` | A prediction outside the 0-1 range the metric lives in | Pick a number in range. `0` and `1.0` are both legal. On pass 1 nothing scores it - write the honest guess and put the content in `--claim` |
+| `1` | `overriding the "continue" advice with "stop" needs a --why of at least 12 characters (got 4).` | Thin override reason | Write the actual reason. It is permanent in `report.md` |
+| `1` | `session "ui-2026-08-24" already exists at …` | `--slug` collision | Pick another slug, or resume: `ponder status --session ` |
+| `1` | ` line 1 is not valid JSON: …` | A truncated or fenced JSONL line | Ask the batch agent for JSONL and nothing else - no prose, no code fences |
+| `0` | No **Prior pondering** section | OCA unreachable, or nothing matched | Expected. `ponder doctor` if you care |
+
+One failure prints no error at all: a `unit-delete` on a never-graded unit succeeds, and coverage does
+not move. That is not a bug to route around. The unit left the inventory and stayed in the coverage
+denominator, exactly as designed - grade it, or claim `"phantom": true` and let the engine check.