Ponder
GitHubOfficialFirst committed by Quinnod345Distributed by GitHub
- Skill
- Workflow
- Agent tooling
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
Ponder is a tool for performing repeated, structured grading passes over a subject. It uses a four-perspective deliberation loop (Skeptic, Builder, Dreamer, Empath) to refine findings, ensure exhaustive coverage, and eliminate unsupported or redundant data. The engine tracks the state of the deliberation across passes, requiring users to justify stopping based on convergence metrics rather than arbitrary completion. It is designed to prevent incomplete sweeps by enforcing unit coverage and maintaining a persistent ledger of changes.
What it does
- Executes iterative grading passes over a defined set of units.
- Applies four distinct cognitive perspectives to evaluate and refine findings.
- Enforces exhaustive coverage to ensure no units are skipped during the audit.
- Tracks churn and convergence to determine when a subject has been fully deliberated.
- Maintains a persistent session ledger to allow for resumption of interrupted tasks.
- Generates a final report detailing findings, corrections, and deliberation arguments.
When to use it
- When you need to perform a broad, multi-pass audit or grade across many units.
- When a single-pass review is insufficient to capture the depth or nuances of a subject.
- When you need to resume a previously started, incomplete auditing session.
- When you want to ensure that all units in a scope are accounted for and graded consistently.
Advantages
- Prevents silent skipping of units through enforced coverage checks.
- Reduces hallucination by requiring evidence-based deliberation from multiple perspectives.
- Provides a clear, objective signal for when a task has converged.
- Supports long-running, complex audits by allowing session resumption without data loss.
Usage examples
- “ponder the UI”
- Initializes a new pondering session for the UI, setting up the necessary directory structure and starting the first pass.
- “ponder status”
- Displays the current progress of an active session, including the current pass number and the next required phase.
- “ponder decide stop --why "The last two passes produced no new findings and all units are covered."”
- Commits the decision to end the session, recording the provided justification in the final report.
- “ponder list”
- Lists any existing pondering sessions to help the user identify whether to resume or start a new one.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/github/ponder/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip ponder.zip -d .agents/skills
Documentation by Quinnod345
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 - runponder listfirst; 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 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 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 andverify --repairwork.references/perspectives.md- Skeptic / Builder / Dreamer / Empath as runnable prompts, and how to fan them out.references/harness-notes.md- what differs between Claude Code and Codex: subagents, sandboxing, psql, install. Also the session lock, in full, if you are about to fan work out.
Read the lens pack for your subject (lenses/<lens>.md in the ponder repo) before pass 1. It defines what a unit is, how to enumerate exhaustively, the dimensions, and what each rung of the 1-5 scale means.
The loop
Once, at the start
First check that you are actually at the start. If a pondering of this subject is already open, start is the wrong command and a second session throws away the first one's ledger.
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 2start 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
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 assurprise = |predicted - actual|. Guessing low every time to look calibrated corrupts the only signal that tells you whether you understand the subject yet.--churnis validated to 0-1 - anything outside that is refused, not clamped:ponder predict --churn 1.2exits1and records nothing.0and1.0are 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.survey (pass 1 only) - enumerate every unit exhaustively, using the lens pack's enumeration recipe, or let the CLI walk disk/git directly with
--scan <dir>.ponder units --file .ponder/tmp/units.json --scan src/routes --strict-scope(orponder units --scan src/routesto auto-seed every file in<dir>) When--scan <dir> --strict-scopeis passed alongside--file, the engine cross-checks everyunit.pathagainst the actual files on disk under<dir>and exits2if any file was omitted. Later passes amend the inventory with deltas instead:unit-addfor what the survey missed,unit-editfor what it got wrong,unit-deletefor what is out of scope.appraise - grade every unit against the lens rubric. Pass 1:
ponder grade --file .ponder/tmp/findings.jsonPass 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.deliberate - put the current finding set to the four perspectives (
references/perspectives.md).ponder deliberate --file .ponder/tmp/deliberation-2.jsonThis 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 eachargumentas 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 inpasses/NN/deliberation.json, surfaces the reason inreport.md, and - when the OCA is reachable - raises adeliberation_skippedmetacognitive observation. The record survives with the OCA off; only the observation does not.revise - apply the deltas the deliberation produced.
ponder delta --file .ponder/tmp/deltas-2.jsonlEvery delta carriesop,perspective, and a non-emptywhy. The engine rejects a delta without awhy. 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-machinedeliberation.jsonarguments anddeltas.jsonlwhystrings using compact causal arrows ("safe-query.js:5 binds $1 -> SQLi claim false -> retract"), while keeping all user-facing findingtitle,detail, andrecommendationfields in clear, complete prose.judge -
ponder judgecomputes the metrics and prints advice.decide -
ponder decide <continue|stop|escalate> --why "..."commits your call.
On pass 2+ the engine skips survey and collapses appraise into the ponder delta call, so the command sequence is predict → deliberate → delta → judge → decide. Phase order is enforced: a command that arrives out of order exits 2 and names the phase you owe. Believe that message over your memory. And if at any point in this list you are not certain which pass you are in or what you already did, stop guessing and run ponder status - it is cheap, it takes no lock, and it answers both (Resumption).
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.3with 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:
coverageis 1.0 (the engine enforces this; see below).The last two passes produced no
addyou would defend as new ground.churnis near zero, and what did move was ranking, not substance.Every open
mustfinding hasconfidence >= 0.5- that isconfidenceFloor 0.00on the judge table, not merely under the 0.2 the engine enforces. You are about to recommend work; amustyou cannot defend is a guess you are asking someone to act on. If the floor is not zero, either go and get the evidence (achangeraising the confidence) or drop the severity.The last pass you would call substantive heard at least three voices.
ponder decide stopwarns 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 adds). 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 betweenunits.jsonandfindings.json; moving it would orphan every finding on that unit. The engine refuses and says so. If the id itself is wrong,unit-deleteandunit-addthe corrected unit, and pay what that costs coverage.metamerges key by key. Correcting a route leaves thestatemarker 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-editcostschurninstead, 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-editappears inreport.mdunder Corrected screens - or whatever your lens calls a unit - with its pass, its voice, the new values and yourwhy. And a label edited by hand inunits.jsonwith no ledger record behind it is a divergence:judge,decide,reportandapplyall 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 asverified-missing; the path was edited, or there was never one → accepted and recorded for ever asunverified. Every accepted phantom is named permanently inreport.mdunder Phantom units, with yourwhyquoted underneath it.
{"op":"unit-delete","target":"screen.legacy.print","perspective":"skeptic","why":"Opened src/routes/legacy/print.tsx - the file is gone; the route was removed in #1841 and pass 1 enumerated it off a stale router snapshot.","phantom":true}Now the part that matters. phantom is a claim that a route does not exist - that pass 1's survey enumerated something the software does not have. It is not a claim that you ran out of context, or time, or patience.
If you did not look at a screen, do not delete it from the list. Go look at it. That is the whole job. The engine can catch you only when the unit has a path to check; a unit with no path is accepted on your word alone, and the record will say unverified next to your why for as long as the session exists. Writing "phantom": true about a route you simply never opened is lying to a file that keeps it - and it is precisely the half-delivered sweep ponder was built to make impossible, done by hand.
Before you write phantom, answer one question: what did I open, and what was not there? If the answer names a path and an absence, put it in the why and file the delta. If the answer is "nothing, I just didn't get to it", you have a unit to grade, not a phantom to declare.
And the path is not a way around that. unit-edit can move a unit's path. Two deltas - point the unit at a filename that does not exist, then claim it never existed - would have the engine certify the absence of a screen that is sitting right there in the repo. That is the same dishonesty as the last paragraph wearing a new op, and the engine closes it: a phantom claim is checked against every path the unit has ever carried - the one its unit-seed or unit-add gave it, every value any unit-edit has set since, the one units.json holds now, and any set earlier in the same batch. One survivor sinks the whole batch, and the refusal names the path it found and the pass that set it:
delta line 3: unit "screen.auth.sso" is claimed phantom, but a path it has carried exists:
/abs/src/routes/auth/sso.tsx (carried by the unit-add that created it in pass 4). Something you
can open is not something pass 1 imagined. A phantom claim is checked against every path the unit
has ever had, so editing the path first does not clear it.Editing a path never strengthens a phantom claim either. If the path moved and no version of it exists, the claim is accepted but permanently recorded unverified - never verified-missing - and report.md says which paths were checked. Every version was missing, and the engine still will not certify it, because "missing" and "never existed" are only the same claim when the target stayed still. So a moved path buys you exactly nothing and leaves a permanent note that the unit moved before it vanished.
If the path in the inventory is genuinely wrong, correct it with a unit-edit and go grade the screen. That is what the op is for. It is not a step on the way to deleting the screen.
And one refusal that is not about you at all. Every path is resolved from the session's project root, so the check is only as good as that root. If the root was guessed - no --project, and no .git / package.json / other project marker anywhere above the directory you started in, so the engine adopted a stray .ponder or fell back to the working directory - then a path that fails to resolve tells you nothing, and the claim is refused:
delta line 2: unit "screen.settings.profile" is claimed phantom and none of the 2 paths it has
carried (src/routes/settings/profile.tsx, src/routes/settings/profile-v2.tsx) resolves under
/private/tmp - but that project root was guessed, not established (no project marker anywhere
above the working directory; adopted an ancestor .ponder). A file that is missing from the wrong
directory is not a screen that never existed.This is the engine refusing to certify its own guess. ponder start and ponder status print the project root and what decided it, so check that line first: if it says the root was adopted or fallen back to, and it is not where the software actually lives, start the session again with --project <path> and the claim will be checked properly. A unit that never had a path is unaffected - there was nothing for a root to resolve, and it is still accepted as unverified on your word.
The honest exits when you genuinely cannot finish are ponder decide continue (do the work next pass) and ponder abort --why "..." (say what stopped you). Both leave a record that reads true later. A false phantom does not.
Fanning out over many units
A sweep of 40 screens does not fit in one context and should not try to. Batch the grading; keep the session single-threaded.
Batching rule: 8-12 units per agent. Fewer wastes a context; more and the last units in the batch get a thinner read than the first - exactly the shallow-grade failure you would then have to spend a pass fixing.
Non-negotiables for fan-out:
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 concurrentponder deltacalls: one applies its batch and advances the phase; the other waits for the lock, reloads the session, finds it now owesjudge, and exits2with 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.mdhas 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
adds 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-<n>.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 -
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." \
</dev/null &backgrounded with
&and collected withwait; or run batches sequentially in one context, writing each batch's JSONL to disk and dropping the detail before starting the next. Sequential is slower but has no subprocess or sandbox surprises, and the disk is your memory either way.codex execstarts a fresh session and inherits none of your context, so everything it needs goes in the prompt or in a file it can read.
Neither of the two extra tokens on that command line is decoration. Without --skip-git-repo-check the child exits 1 before it does any work - Not inside a trusted directory and --skip-git-repo-check was not specified - so a ponder of anything that is not a git checkout loses every batch at once. And codex exec reads stdin even when the prompt is an argument; backgrounded with & it inherits the parent's stdin, so </dev/null is what stops a batch blocking on a pipe nobody is going to close.
Then, in either harness, concatenate once and hand the engine a single file. ponder grade takes JSON, not JSONL, so the batch lines need wrapping:
cat .ponder/tmp/batch/findings-*.jsonl > .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.jsonsetEncoding("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:
cat .ponder/tmp/batch/deltas-2-*.jsonl > .ponder/tmp/deltas-2.jsonl # pattern cannot match the target
ponder delta --file .ponder/tmp/deltas-2.jsonlDeliberation 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.
ponder statusponder 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 "<reason>"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.
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/<slug>/findings.json # 3. the current truth - every live finding, with ids
cat .ponder/<slug>/ledger.jsonl # 4. how it got there: every delta, every why, in order
ponder report # 5. optional, when you want it readable rather than rawThen 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 --jsonfor the same thing machine-readable, including the ungraded unit list.ponder listshows 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 <slug>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.jsonorledger.jsonlis 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 doctorreconciles every session it can see and names any that fail under Problems, with theponder verify --session <slug>that diagnoses each one. That is how a machine full of sessions tells you which one is damaged.Never hand-edit
units.jsonorfindings.jsonto 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 atjudge. 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
2and 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.
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):
{ "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" } }
] }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.
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{ "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}}ponder delta --file .ponder/tmp/deltas-2.jsonl # 11 deltas: 3 add, 3 edit, 2 change, 2 delete, 1 unit-add
ponder judgeponder 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 "<reason>" --session ui-2026-08-24 # blocked: 1 ungraded screen - stop is refused until coverage is 1.0The 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.
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.
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.jsonGrading 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"}}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.
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 judgeA 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 "<reason>" --session ui-2026-08-24 # the engine advises stopponder 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,shouldOnly 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 - andvoices 4/4does 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.deleteing a finding because it is low priority. Delete is for unsupported, duplicated, or wrong. Low priority is achangetoseverityorstatus: deferred- retracting it hides it from the ranking instead of ranking it.Deleting a unit from the inventory to clear a
blockedverdict. It does not clear it - a never-graded unit stays in the coverage denominator.Claiming
"phantom": trueon a route you never opened. Phantom means the thing does not exist. If you did not look, go look.unit-editing 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
editis churn you have to defend inreport.mdfor ever.Hand-editing
units.jsonorfindings.json. It is a divergence, not a fix -judge,decide,reportandapplyrefuse 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-editedmetrics.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 statusand theNEXT:line show the truth.
Files
- ATTRIBUTION.mdAdded by AutoGineer849 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md48.5 KiB
- references/harness-notes.md14.4 KiB
- references/perspectives.md23.2 KiB
- references/protocol.md50.5 KiB
references/protocol.md
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:
ln -s ~/code/ponder/bin/ponder ~/.local/bin/ponder # ~/.local/bin is already on PATHEvery command accepts:
| Flag | Meaning |
|---|---|
--session <slug> | 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 <command>. |
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 <project>/.ponder/<slug>/, falling back to ~/.ponder/<slug>/ when the project
path is not writable. Which directory <project> is - and what decided that - is under
ponder start; it is printed every time, and it is what every
relative path in the session resolves against.
.ponder/<slug>/
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 onlyNothing 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.
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 "<subject>" --lens <lens>
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 <name> | required | ui, architecture, docs, tests, product, copy, custom |
--rubric <file> | - | required with --lens custom; any file with the lens front-matter shape |
--project <path> | resolved (below) | The root every relative path in the session resolves against, and where .ponder/ is rooted |
--slug <slug> | <subject-slug>-<date> | 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-24A 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 <path> 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 "<one line>"
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 <units.json> / --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 <findings.json> / --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 <deliberation.json> / --stdin
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 <deltas.jsonl> / --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 <continue|stop|escalate> --why "<reason>"
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 toverdict.jsonand, for overrides, surfaced permanently inreport.mdunder Overrides.You may override the advice, except:
stopexits2and 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 fromunits.jsonandfindings.jsonat decision time, so a stale or hand-editedmetrics.json/verdict.jsonchanges nothing.escalatewhile 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.stopwhilevoices < 3warns 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
stopon reasonconvergedcloses the session asconverged; every other stop -budget,perseveration, an override - closes it asstopped, andreport.mdsays 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 recordRun 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 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 <path>]
Regenerates the readable summary from current state, in this order: Convergence, Overrides,
Prediction vs reality, Coverage (with Corrected <units> 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/<slug>/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 <slug> 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 "<reason>" / 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 <slug> 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 <command> 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
{
"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)
{
"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
{
"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 <slug> pass N: revise the finding set for "<subject>"), 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.
{"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-editis 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 withunit-delete+unit-addwould 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 afield-revertexactly like a severity flipped back and forth. Reversal is judged on the effective value, so two edits touching differentmetakeys are not a false reversal.The record: permanent. Every
unit-editappears inreport.mdunder Corrected <units> with its pass, its perspective, each field's new value and itswhy- the same treatment a removal gets. A hand-edited label inunits.jsonwith no ledger record behind it is a divergence, and every decision-point command refuses the session until it reconciles - whichverify --repairdoes 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 "<id>" is claimed phantom, but a path it has carried exists: <abs> (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 <root> - 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:
{ "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 \"<reason>\" …" } }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 ledgerWhat --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 adds 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 adds 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 adds a finding materially matching one an earlier pass deleted (same unit + same
dimension); or when it deletes one added within the previous two passes.
Verdict rules, in order
First match wins.
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.pass < minPasses→continue. A single pass is an opinion, not a pondering.thrash > 0.25→escalate. Another identical pass will not help - rotate perspectives, widen the evidence, or split the subject. Skipped whenthrashis null.Evidence and review guard (
confidenceFloor > 0orvoices < 3): any openmustfinding below 0.5 confidence (confidenceFloor > 0) or fewer than three reviewing perspectives (voices < 3) blocks convergence → ifpass >= maxPasses,stopwith reasonbudget; else ifstall >= 2,escalate; elsecontinuewith reasonlow-confidenceorinsufficient-perspectives. Stillness with unresolved uncertainty does not earn convergence, and no quiet pass bypasses this guard.novelty < 0.05andchurn < 0.10, withvoices >= 3andconfidenceFloor == 0, sustained two consecutive passes →stop, reasonconverged(engine rule id6). Checked before the fallback perseveration stop so two fully reviewed, confident quiet passes earnconverged.stall >= 2andcoverage == 1.0→stop, reasonperseveration(engine rule id4). Nothing is moving, and the two-pass convergence check did not match (for example, because the prior pass lacked full perspective review or confidence).stallonly counts passes whose churn was a number, so a pass with no churn cannot feed it.pass >= maxPasses→stop, reasonbudget. Report it as a budget stop, never as convergence.otherwise →
continue, reasondefault- unless the OCA is holding open metacognitive observations of high severity about this loop, which tip an otherwise-continueintoescalate, reasonoca-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/<name>.md, read by the agent at survey and appraise:
---
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 <file> 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/<slug>/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 <slug> is still open on pass N (<phase>). | ponder apply without --dry-run on an open session | --dry-run to preview, or finish the pondering first |
2 | session <slug> 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 "<id>" is claimed phantom, but a path it has carried exists: <abs> (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 "<id>" is claimed phantom and none of the 2 paths it has carried (…) resolves under <root> - 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 <path>; 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 "<id>" 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 <slug> does not reconcile with its ledger - 2 divergences. | judge, decide, report or apply on state that disagrees with the ledger | ponder verify --session <slug> 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 <slug> |
1 | <path> 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.
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Names Codex tools, Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- First committed by
Quinnod345
- Publisher
- GitHub (Official)
- Distributed by
- GitHub
- Source
- github/awesome-copilot · skills/ponder · commit 797337f
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- Quinnod345
AutoGineer is not affiliated with or endorsed by Quinnod345 or GitHub.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download ponder.zip