Ponder is a convergent deliberation loop for coding agents. An agent runs repeated passes over a subject. Each pass revises the previous pass’s output — adding what was missed, editing what was vague, re-grading what was mis-scored, deleting what was noise — until further passes stop changing the answer. The agent decides how many passes to run; the engine measures whether stopping is honest and refuses to let a pass be silently skipped.
This document is the contract. bin/ponder, oca/bridge.js, the lens packs, the skill,
and the conformance suite are all written against it. Where an implementation and this
document disagree, this document wins.
| Term | Meaning |
|---|---|
| session | One pondering of one subject through one lens. Lives in .ponder/<slug>/. |
| subject | What is being pondered. Free text: "the UI", "our error handling". |
| lens | The rubric pack that defines how to enumerate and grade. ui, architecture, docs, tests, product, copy, custom. |
| unit | One gradable thing inside the subject. For ui, one screen/route/view. |
| finding | One graded judgment about one unit. The atom that passes revise. |
| pass | One full cycle: predict → survey → appraise → deliberate → revise → metacognize → judge → decide. |
| delta | One revision operation applied to the finding set (add, edit, change, delete) or to the inventory (unit-add, unit-edit, unit-delete). |
| verdict | The engine’s convergence advice for the pass: continue, stop, escalate, blocked. |
| decision | The agent’s committed choice, which may override the advice, plus a written reason. |
Ponder’s loop is the Oneiro Cognitive Architecture’s loop, applied to a subject instead of to a machine’s own existence. Each phase names the OCA layer it borrows from.
| Phase | OCA layer | What happens |
|---|---|---|
| predict | L5 Hypothesis | Before working, the agent states what this pass will find: expected churn (0–1) and a one-line claim. Recorded, then scored against reality. Falling surprise means the loop now models itself. |
| survey | L1 Sensory | Enumerate the units. Pass 1 builds the inventory; later passes may amend it (an amendment is itself a delta). |
| appraise | L5 Hypothesis | Grade each unit against the lens rubric. Pass 1 creates findings; later passes revise them. |
| deliberate | L8 Adversarial Deliberation | Four perspectives argue over the current finding set. |
| revise | L10 Executive Control | Apply deltas. Every op appends to the ledger, forever. |
| metacognize | L7 Metacognition | Measure this pass against history: novelty, churn, thrash, surprise, stall, coverage. Detect stuck and perseveration. |
| judge | L7 + L10 | Engine computes advice. Agent commits a decision with a reason. |
This is the load-bearing idea. OCA’s deliberation engine runs Skeptic, Builder, Dreamer and Empath over every decision. Mapped onto a finding set, each perspective naturally produces exactly one kind of revision:
| Perspective | OCA loss function | Asks | Emits |
|---|---|---|---|
| Skeptic | minimise false positives | “What here is unsupported, duplicated, or just wrong?” | delete |
| Builder | maximise shipped output | “Is this actionable? What’s the concrete next step?” | edit |
| Dreamer | maximise novelty | “What did everyone miss? What if the frame is wrong?” | add |
| Empath | maximise social accuracy | “Does this serve the person using it? Is it ranked right?” | change |
A pass that produces only add is a pass that never argued with itself. A healthy pass
shows all four voices.
Sessions live under <project>/.ponder/<slug>/, or ~/.ponder/<slug>/ when the project
path is not a directory the agent can write to.
.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
passes/NN/
predict.json the pass's own prediction, written before 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 generated by `ponder report`
session.json{
"version": 1,
"slug": "ui-2026-08-24",
"subject": "the UI",
"lens": "ui",
"project": "/abs/path",
"projectFrom": { "how": "marker", "marker": ".git", "cwd": "/abs/path/src/routes" },
"maxPasses": 6,
"minPasses": 2,
"createdAt": "2026-08-24T15:00:00.000Z",
"updatedAt": "2026-08-24T15:42:00.000Z",
"status": "open",
"currentPass": 3,
"phase": "deliberate",
"oca": { "enabled": true, "reachable": true, "sessionRef": 12345 },
"history": [
{ "pass": 1, "verdict": "continue", "decision": "continue", "reason": "coverage 0.6" }
]
}
status: open | converged | stopped | aborted | applied.
phase: the next phase the agent owes. Advanced by the command that satisfies it.
project is the directory every relative path in the session resolves against — every unit
path, every finding evidence anchor, and every phantom claim checked against one (3.5).
A wrong root does not raise an error: it makes every path check quietly return “not found”,
which is the wrong answer in the direction that lets a session stop early.
ponder start --project <path> sets it outright. Otherwise the engine resolves it from the
working directory in this order, and the order is the contract:
.git, .hg, .svn, package.json, go.mod,
Cargo.toml, pyproject.toml, deno.json, deno.jsonc, Gemfile, pom.xml,
build.gradle, build.gradle.kts, composer.json, mix.exs, or a directory named
*.xcodeproj / *.xcworkspace. The nearest directory holding one is the project root,
full stop..ponder. With no project to anchor to, adopting a session store that is already there
is the most useful thing available — but it is a guess..ponder is deliberately not in step 1. .git defines what the project is; .ponder is
only where its sessions are kept, and it must never outrank the thing it belongs to. A single
walk over both kinds of marker lets an unrelated ancestor .ponder capture any directory
with no markers of its own — which is exactly where scratch work happens.
projectFrom records the answer to “how do we know?”, so a session can be inherited without
re-deriving it:
how |
Meaning | marker |
|---|---|---|
explicit |
--project was given |
null |
marker |
a project marker decided it | the marker’s name, e.g. .git |
ponder |
no marker anywhere; an existing .ponder was adopted |
.ponder |
cwd |
nothing was found; the working directory is the root | null |
cwd also carries the directory the resolving command was run in.
A root is established when how is explicit or marker, and guessed when it is
ponder or cwd. A guessed root makes every path miss ambiguous, which is why 3.5 refuses a
phantom claim under one. ponder start, ponder status and ponder doctor all print the
root and what decided it; start and status warn when it was guessed, and doctor reports
an adopted ancestor .ponder as a problem.
A session written before this field existed does not carry it, and is never refused or warned about for that. Its origin is re-derived from the recorded root: a root that holds a project marker was a sound root however it was found, and one that does not was a guess. The field is engine bookkeeping — the ledger (3.6) knows nothing about it, so its absence, presence or value cannot affect reconciliation.
There is exactly one mode. Ponder deliberates and produces a plan; ponder apply is the
separate explicit step that turns the plan into work. A session written by an older build may
still carry a mode field — it is ignored on load and never written back.
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" }, "addedInPass": 1 }
]
}
id is stable across passes and is the join key for findings. Required, unique,
lowercase dotted slug. id is the one field nothing can change — a unit-edit that
tries is rejected, exit 1, because changing it would orphan every finding on that unit
(3.4). path optional but strongly preferred — it is the evidence anchor, and it is what
lets the engine check a phantom claim (3.5).
label, path, kind and meta are all correctable by a later pass with a unit-edit
delta. A unit whose caption went stale is fixed in place, on the record, rather than by a
unit-delete plus a unit-add — that pair would drop coverage below 1.0 and block the
stop, which turns a wrong caption into a reopened session.
A unit that has left the inventory keeps its entry and gains:
| Field | Meaning |
|---|---|
removedInPass |
the pass whose unit-delete removed it |
removalKind |
appraised | removed-ungraded | phantom — see 3.5 |
phantomVerification |
on a phantom only: verified-missing | unverified |
phantomPath |
on a verified-missing phantom: the absolute path the engine checked |
A session written before removal kinds existed carries only removedInPass. Its kind is
inferred on read the same way the engine would decide it today — appraised if the unit
carries any finding, removed-ungraded otherwise — so an old session cannot inherit an
escape the current rules close.
findings.json{
"version": 1,
"findings": [
{
"id": "f-0007",
"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.",
"status": "open",
"createdInPass": 1,
"lastTouchedPass": 3,
"history": [{ "pass": 3, "op": "change", "field": "severity", "from": "should", "to": "must" }]
}
]
}
score: integer 1–5. 1 = broken, 3 = acceptable, 5 = exemplary. Lens packs define each rung.severity: must |
should |
could |
note. |
confidence: 0–1. Below 0.5 the finding is a hypothesis, not a conclusion.effort: xs |
s |
m |
l |
xl. |
status: open |
resolved |
retracted |
deferred. |
id format f-NNNN, assigned by the engine, never reused.deltas.jsonl / ledger.jsonlOne JSON object per line.
{"pass":3,"op":"delete","target":"f-0012","perspective":"skeptic","why":"Duplicate of f-0004.","at":"2026-08-24T15:40:00.000Z"}
{"pass":3,"op":"add","perspective":"dreamer","why":"No pass had looked at loading states as a set.","finding":{ "...": "a full finding object without id" },"at":"..."}
{"pass":3,"op":"edit","target":"f-0007","perspective":"builder","why":"Recommendation was not actionable.","set":{"recommendation":"..."},"at":"..."}
{"pass":3,"op":"change","target":"f-0009","perspective":"empath","why":"Blocks first-run, outranks polish.","set":{"severity":"must","score":2},"at":"..."}
{"pass":3,"op":"unit-add","perspective":"dreamer","why":"Route was missed in pass 1.","unit":{ "...": "a full unit object" },"at":"..."}
{"pass":3,"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":"/"}},"at":"..."}
{"pass":3,"op":"unit-delete","target":"screen.settings.profile","perspective":"skeptic","why":"Owned by another team; out of this subject.","removal":"appraised","at":"..."}
{"pass":3,"op":"unit-delete","target":"screen.auth.sso","phantom":true,"perspective":"skeptic","why":"Pass 1 read this route off a stale sitemap; the app has no SSO screen.","removal":"phantom","verification":"verified-missing","verifiedPath":"/abs/path/src/routes/auth/sso.tsx","at":"..."}
{"pass":1,"op":"unit-seed","source":"units","perspective":null,"why":"Pass 1 enumerated 14 screens as the coverage denominator.","ids":["screen.settings.billing","..."],"units":[{ "...": "each full unit object, as stored" }],"at":"..."}
Nothing enters a session’s state without a ledger line. That is what makes 3.6 possible,
and it is why unit-seed exists: pass-1 findings have always arrived as add records tagged
source: "grade", and every later inventory amendment is a unit-add, unit-edit or
unit-delete delta, but pass 1’s inventory used to be written straight into units.json
and nowhere else.
An unledgered inventory is a coverage denominator taken on trust.
unit-seed is written by ponder units, once, on pass 1. It carries ids — the enumerated
unit ids, in order — and units, their full bodies exactly as units.json stores them.
It is a seed, not a delta: it is not one of the seven delta ops, no delta payload may
contain it, and it is excluded from every convergence ratio and from the delta counts in
metrics.json and report.md, the same way source: "grade" seeding is.
Rules:
add carries a whole finding object minus id; the engine assigns the id.edit and change carry set, a partial finding. edit is for prose and evidence;
change is for score, severity, confidence, effort and status. The engine does not
enforce which fields go with which op, but the conformance suite warns when change
edits only prose, because that hides re-grading from the thrash detector.unit-edit carries set, a partial unit. It may set label, path, kind and meta,
and it may never set id — the id is the join key between units.json and
findings.json, so changing it would orphan every finding on that unit. An attempted id
change is rejected, exit 1, with a message saying why. If the id itself is wrong, delete
the unit and add the corrected one, and accept what that costs coverage.
meta merges key by key rather than replacing wholesale, so a unit’s route can be
corrected without wiping the state marker sitting beside it in the same object. Every
other field replaces. Replay applies the same merge, so the ledger derives the state the
engine wrote (3.6).unit-edit is a revising delta: it goes on the ledger, it counts toward churn,
and it participates in thrash detection — a label edited back and forth is oscillation
like any other (4).unit-edit does not touch coverage. It is neither an addition nor a removal, so the
denominator and the numerator are both exactly what they were. It is also refused on a
unit that has already left the inventory: a removed unit is re-added, not edited.delete sets status: "retracted". Findings are never removed from findings.json.
Retraction is a state, not an erasure — a later pass must be able to see, and reverse,
what an earlier pass killed.why. The engine rejects deltas without one. So does
unit-seed, which is a line on the ledger like any other.ledger.jsonl is the union of all passes’ deltas.jsonl, never rewritten.unit-delete and the phantom claimunit-delete is the one operation that can shrink the coverage denominator, which makes it
the closest thing in the system to a coverage override. It is governed accordingly.
Every unit-delete needs a non-empty why, like every other delta. What it costs beyond
that depends on whether the unit was ever appraised. Appraised means the unit carries at
least one finding — a retracted one counts, because withdrawing a judgment does not un-see
the thing it judged.
| The unit | The delta carries | What happens | Coverage denominator |
|---|---|---|---|
| has ≥ 1 finding | no phantom |
removalKind: "appraised" |
shrinks — an honest scope change |
| has ≥ 1 finding | "phantom": true |
rejected, exit 1 — it was appraised, so it existed | unchanged |
| has no findings | no phantom |
removalKind: "removed-ungraded" |
unchanged — the unit still counts |
| has no findings | "phantom": true |
the engine verifies the claim (below) | shrinks once verified |
"phantom": true means: this unit does not exist in the software; pass 1 enumerated it
wrongly. It is a claim about the world, so the engine checks it rather than trusting it —
and it checks it against every path the unit has ever carried, not just the one
units.json holds now.
That distinction is load-bearing. unit-edit can move a unit’s path (3.4), 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. So the candidate set is the original path from the
unit’s unit-seed or unit-add ledger record, plus every value any unit-edit has set
since, plus whatever units.json holds now — read off the ledger, resolved against
session.project, deduplicated. A path set earlier in the same delta batch counts too.
| The unit’s paths | Result |
|---|---|
| any of them exists | the whole delta batch is rejected, exit 1. The message names the path that was found and the pass that set it. Something you can open is not something pass 1 imagined. |
| exactly one, never edited, and it does not exist | accepted · phantomVerification: "verified-missing", phantomPath = the path checked |
| the path was ever edited, and no version of it exists | accepted · phantomVerification: "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: “missing” and “never existed” are only the same claim when the target stayed still. The ledger record carries pathsChecked so the report can say exactly what was looked for. |
| the unit never had a path | accepted · phantomVerification: "unverified" — the claim could not be checked at all |
Editing a path is therefore never a way to strengthen a phantom claim: at best it leaves the claim where it would have been with no path at all, and at worst it is refused outright.
Those rows assume the paths were resolved against a project root that was established
— --project, or a project marker (3.1). Under a guessed root, a unit with at least one
path and no path that resolves is a different situation entirely: nothing was really checked.
The claim is refused, exit 1, and the refusal names the resolved root, says it was
guessed rather than established, and names --project as the fix. This is not a judgment
about the agent’s honesty — it is that a file missing from the wrong directory is not
evidence about a screen. A unit that never had a path is unaffected: there was nothing for a
root to resolve, so it is still accepted as unverified.
That closes the gap this rule exists to close. path verification is what makes a phantom
claim a check rather than an assertion, so a resolution that silently misses every path turns
verification into a rubber stamp — coverage 1.0 and a permitted stop, bought by a root the
user never chose and was never shown.
Because “appraised” is defined as carrying at least one finding, the engine re-derives it
rather than believing it. removalKind is read back off units.json, and both of the kinds
that shrink the denominator are read from there, so a stored appraised on a unit with no
findings contradicts its own definition. When the field and the definition disagree the
definition wins and the unit stays in the denominator — whether the disagreement arrived by a
hand-edit or by a bug. That re-derivation is the inner defence and it is deliberately kept:
reconciliation (3.6) catches a forged removalKind outright and refuses the command, but if
it ever failed to, the denominator would still not shrink.
phantom is legal on unit-delete only, and only on a unit with no findings: a unit that
was appraised cannot also be one that never existed. A removed-ungraded unit may be deleted
a second time with "phantom": true — that is the upgrade path from “I dropped it” to “I
checked, and it is not there.” No other repeat delete is legal.
Every accepted phantom is listed permanently in report.md under Phantom units with its
why, its pass and its verification result. An unverified phantom is marked distinctly
there, because it is the one claim nobody checked.
The point: dropping thirty screens you never looked at costs thirty explicit, individually justified, permanently reported phantom claims that the engine checks wherever it can. It is not free and it is not silent.
Everything in 3.5 governs what the engine will do. None of it governs a text editor.
units.json and findings.json are ordinary files, and an agent that wants past the
coverage gate can write to them directly: forge removalKind: "phantom" onto a unit nobody
graded, delete a unit’s entry outright, invent a finding, flip a status from open to
resolved. Each of those is a lie the engine would previously have believed, because it read
the gate’s inputs out of the very files the lie was written into.
ledger.jsonl is append-only and records every operation the session has ever applied (3.4).
So the honest state is not something to be trusted — it is something to be recomputed.
Reconciliation 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. Any disagreement is a divergence, reported with the id, the field, what the
ledger says and what the file says.
It then checks the ledger against itself. ledger.jsonl is the ordered union of the
per-pass deltas.jsonl files (3.4) — ponder delta, ponder grade and ponder units
write both copies in one call — so the per-pass files are an independent second copy of
every record. Reconciliation compares them record for record, in order, grouped by pass.
That is what catches the forgeries a replay alone cannot see, because they add no
contradiction to the ledger:
ledger.jsonl so it agrees with an edited state file —
the line is in one copy and not the other;ledger.jsonl reordered, which changes what a replay derives without writing a new
record;ledger.jsonl truncated, so a pass’s work stops being on the record;What the ledger pins, and is therefore reconciled:
| 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 |
Every one of those is determined: an add record carries the whole finding body after
normalisation, every edit / change carries the exact set that was applied, delete
means status: "retracted", unit-seed and unit-add carry unit bodies as stored,
unit-edit carries the exact set it applied — replayed with the same key-by-key meta
merge the engine used, so a corrected route derives identically — and unit-delete carries
the removal kind and the phantom verification.
Because label, path, kind and meta are all replayed from unit-edit records, a
label changed by hand in units.json with no matching ledger record is a divergence and is
refused exactly like a forged removalKind. Correcting a caption is cheap; correcting one
off the record is not possible. path is in the list
because a phantom claim is checked against it — a unit that quietly loses its path is a unit
whose phantom claim nobody can check.
Deliberately not reconciled: lastTouchedPass and a finding’s history. The ledger
implies both, but they are derived bookkeeping — a disagreement there cannot buy anyone
coverage, a finding, or a line in the apply plan, and pinning derived presentation is how a
reconciler starts producing false alarms. Timestamps are not reconciled either. Neither is
session.json: the ledger records operations on units and findings, and it does not record
the phase machine.
Enforcement. judge, decide, report and apply reconcile before they act, and a
divergence is exit 2 naming what diverged. Those four are the commands that 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.
ponder verify [--repair] [--session <slug>]
verify runs the reconciliation on demand and reports: exit 0 and a summary when the
session reconciles, exit 2 and the full diagnosis when it does not. It is the command to
run first on a session you inherited. ponder doctor reconciles every session it can see and
lists any that fail.
Write ordering, and the crash window it leaves. Every command that changes state appends
to ledger.jsonl first and writes units.json, findings.json and
passes/NN/deltas.jsonl after. The order is not arbitrary and it is not symmetric,
because the ledger is the authority:
So the ordering makes the only reachable crash state the recoverable one, and the engine is never in a position where a torn write costs a pass’s work.
ponder verify --repair repairs a ledger-ahead session. It replays ledger.jsonl,
rewrites units.json and findings.json to the state those records must have produced, and
appends to any passes/NN/deltas.jsonl that is a prefix of what the ledger holds for that
pass. It then re-reconciles and reports, line by line, exactly what it rebuilt. A repaired
session is byte-identical to the one the interrupted command would have written.
--repair refuses a state-ahead session, and that stays a hard error, exit 2:
What --repair found |
What it does |
|---|---|
| a unit or finding the ledger created that the state file lacks, or a field whose stored value is behind the ledger’s | rebuilds it |
a passes/NN/deltas.jsonl holding a strict prefix of the ledger’s records for that pass |
appends the missing tail |
| a unit or finding in a state file that no ledger record ever created | refuses — the data exists in no other copy, so nothing can rebuild it. 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 — that is two copies of an append-only log disagreeing, not one lagging |
a problem inside ledger.jsonl itself, or a line of it that will not parse |
refuses — repair rebuilds from the ledger, so a broken ledger is nothing to rebuild from |
Without --repair, verify never writes anything.
What this guarantees. The ledger is the authority; the state files are a view of it. A forged state file is detected rather than believed, and every command that would draw a conclusion from it refuses.
What it does not guarantee. It is not a signature and not a hash chain. Someone who
edits every copy consistently — the state file, ledger.jsonl, and the matching
passes/NN/deltas.jsonl, at the right position — defeats it, and nothing in ponder stops
them. That is outside the threat model, and it is stated here rather than implied away.
What reconciliation does is raise the cost of a dishonest stop: from one edit to a
state file, to a consistent forgery across three files, one of which is an append-only log
that report.md quotes and the OCA may already have read.
Two boundaries are worth naming explicitly. session.json is not reconciled — the ledger
records operations on units and findings, not the phase machine — so a hand-edited
status or history is not detected by this mechanism, and --repair does not rebuild it
either. And a session whose inventory was
seeded before unit-seed existed has no ledger record for its units and will not reconcile;
re-seed it rather than working around it.
Computed by ponder judge for pass N and written to passes/NN/metrics.json.
| Signal | Formula | Reads as |
|---|---|---|
coverage |
graded units ÷ counted units | Did we actually look at everything? |
novelty |
revising add deltas ÷ live findings |
Still finding new ground? |
churn |
revising deltas ÷ live findings | How much moved this pass? |
thrash |
revising deltas that reverse an earlier delta on the same target ÷ revising deltas | Oscillating instead of converging? |
surprise |
|predicted churn − actual churn| | 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 |
count of distinct perspectives in this pass’s deliberation.json |
Did the pass argue with itself? |
Counted units. The coverage denominator is every unit still in scope: every live unit,
plus every unit removed with removalKind: "removed-ungraded" (3.5). A unit leaves the
denominator only by being appraised and then dropped, or by being an accepted phantom.
Deleting what you never looked at does not improve coverage, and correcting a unit with a
unit-edit does not move coverage at all — it is neither an addition nor a removal, so
both sides of the ratio are exactly what they were.
Revising deltas. Pass 1’s ponder grade seeding is written to the ledger as add
deltas tagged source: "grade" so that every finding has a creation record — the thrash
detector reads creation values as the first setting of a field. Those deltas are the
baseline, not a revision of one, so they are excluded from the numerator of every ratio
above, in every pass. They stay on the ledger for provenance and are reported separately
as deltas.seeded.
Pass 1 has no novelty, churn, thrash or surprise. All four measure change relative to a
previous pass, and pass 1 has none. They are emitted as null in metrics.json and rendered
as — in human output — never as 0.00, which would read as a healthy quiet pass, and never
as a 1.00 computed against the pass’s own baseline. surprise follows churn: pass 1’s
prediction is recorded and shown, and scored from pass 2 onward. No verdict rule may read a
null signal; each threshold below is guarded on the signal actually being a number.
voices counts who spoke, not who emitted a delta. A perspective that argues “nothing to
change here” has spoken and counts. Deltas keep their perspective attribution — that is
what the ledger and the thrash detector read, and it is reported alongside as deltaVoices —
but counting deltas made every genuine convergence look silent: rule 6 caps churn below 0.10,
which on a small finding set permits at most one delta, which forces voices == 1.
A delta reverses an earlier delta when, on the same target, an earlier pass set a
field to value X, a later pass set it to something else, 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 the unit’s seeded or added values counting as the first setting, and meta
compared on its merged value rather than on the partial object that was submitted;
or when this pass adds a finding materially matching one an earlier pass deleted
(same unit + same dimension), or deletes one an earlier pass added within the
previous two passes.
Evaluate top to bottom; first match wins.
coverage < 1.0 → blocked. Stopping is not permitted. The verdict names every
ungraded unit, and names apart the ones that were deleted without ever being graded,
saying why they still count (3.5). This is the anti-half-delivery gate and it has no
override.pass < minPasses → continue. A single pass is an opinion, not a pondering.thrash > 0.25 → escalate. The loop is oscillating. Another identical pass will
not help; rotate perspectives, widen evidence, or split the subject. Skipped when
thrash is null.
3a. Evidence/review guard: any open must below 0.5 confidence or fewer than three
perspectives → continue; if stalled, escalate; if the budget is exhausted,
stop with reason budget. No quiet pass bypasses this guard.stall >= 2 and coverage == 1.0 → stop, reason perseveration. Nothing is
moving. Further passes are theatre. stall only ever counts passes whose churn was a
number, so a pass with no churn cannot contribute to it.confidenceFloor > 0 → continue. Too many important findings are still guesses.novelty < 0.05 and churn < 0.10, sustained for 2 consecutive passes → stop,
reason converged. Both passes must have at least three voices and no low-confidence must.pass >= maxPasses → stop, reason budget. Reported as a budget stop, never as
convergence.continue.ponder judge advises. ponder decide <continue|stop|escalate> --why "..." commits.
blocked, which cannot be overridden by
stop. Attempting it exits non-zero with the ungraded unit list.--why of at least 12 characters. It is written to
verdict.json and surfaced permanently in report.md under Overrides.ponder decide stop while voices < 3 prints a warning: a pass that heard fewer than
three perspectives is a weak place to stop. It does not block. voices is who spoke in
deliberation.json (SPEC 4), so a perspective that argued “nothing to change here” clears
the bar and a genuine convergence is not warned at for being quiet.This is the whole point: the harness cannot silently quit, and the agent is never railroaded into a loop it can justify leaving.
All commands take --session <slug> (defaults to the most recently touched open session
in the current project) and --json (machine-readable output on stdout). All commands
exit 0 on success, 1 on user error, 2 on state-machine violation.
Every command prints a NEXT: line naming the phase the agent now owes. The engine
drives the agent. That line is the mechanism that survives context compaction.
ponder start "<subject>" --lens <lens> [--rubric <file>] [--project <path>]
[--slug <slug>] [--max-passes N] [--min-passes N]
ponder units --file <units.json> # or --stdin. Pass 1 only; later passes use unit-add / unit-edit / unit-delete deltas.
ponder predict --churn <0-1> --claim "<one line>"
ponder grade --file <findings.json> # pass 1 seeding of the finding set
ponder deliberate --file <deliberation.json> # or --stdin, or --skip --why "<reason>"
ponder delta --file <deltas.jsonl> # or --stdin
ponder judge # compute metrics, print advice
ponder decide <continue|stop|escalate> --why "<reason>"
ponder status # where am I, what do I owe
ponder verify [--repair] # replay the ledger, check units.json and findings.json against it; --repair rebuilds a ledger-ahead session (3.6)
ponder report [--out <path>]
ponder apply [--dry-run] [--severity must,should]
ponder list
ponder abort --why "<reason>"
ponder doctor # environment + OCA reachability
ponder version # print the engine version
ponder help lists every one of these, version included. A command that runs but is
documented nowhere is a command nobody can find.
session.phase names what the agent owes next. Commands that arrive out of order exit 2
with a message naming the expected phase. The legal cycle per pass:
predict → survey → appraise → deliberate → revise → judge → decide → (next pass) predict
survey is satisfied by ponder units on pass 1, and skipped automatically on later
passes (unit amendments ride in as unit-add / unit-edit / unit-delete deltas — see
3.4 for what a unit-edit may set and 3.5 for what a unit-delete costs).appraise is satisfied by ponder grade on pass 1, and by ponder delta on later
passes (which satisfies revise too — one command, since later passes appraise by
revising).deliberate may be skipped with ponder deliberate --skip --why "...", recorded as a
metacognitive observation of type deliberation_skipped.ponder applySeparate and explicit. Never runs as part of a pass. Reads findings.json, selects
status: open findings at or above --severity (default must), and emits an ordered
work plan grouped by unit, with evidence paths and recommendations. In --dry-run it
prints the plan. Without it, it prints the plan and writes apply-plan.md, marks the
session applied, and records an OCA episodic entry. Ponder itself never edits the
subject’s source — it hands the agent an ordered plan and the agent does the work.
apply refuses an aborted session, exit 2, with or without --dry-run. An abandoned
pondering must not be able to quietly become an applied one. An open session is refused for
a committed apply too (finish it, or preview with --dry-run).
Optional, degrades silently, zero npm dependencies — it shells out to psql.
PONDER_OCA = auto (default) | on | off. Under auto, one 2-second reachability
probe per process; failure disables the bridge for that process and is never fatal.PONDER_OCA_DSN default postgres://localhost/oneiro.on, an unreachable OCA is a hard error — for CI and for deliberate coupling.| When | Table | Row |
|---|---|---|
start |
episodic_memory |
event_type='ponder_start', content=subject + lens, importance_score=0.7 |
each pass, at judge |
episodic_memory |
event_type='ponder_pass', prediction=predicted churn + claim, actual_outcome=actual churn + delta counts, surprise_magnitude=surprise |
each pass from 2 on, at judge |
hypotheses |
domain='ponder:<slug>', claim=the pass’s convergence claim, prediction, confidence, status, and on the following pass actual_outcome + surprise_magnitude + model_update |
deliberate |
deliberations |
the four arguments and confidences, resolution=the pass’s delta summary, resolution_method='ponder-pass' |
| metacognize, when triggered | metacognitive_observations |
target_layer='ponder:<slug>', observation_type ∈ stuck_state, perseveration, thrash, low_confidence, deliberation_skipped, plus severity and recommended_intervention |
deliberate, judge |
cognitive_events |
event_type ∈ deliberation_result, hypothesis_formed, hypothesis_tested, metacognition_alert, reasoning_trace_created; source_layer='ponder' |
decide stop |
episodic_memory |
ponder_converged only for real convergence; otherwise ponder_stopped, with final counts |
cognitive_events.event_type is a Postgres enum. The live enum carries 33 labels; ponder
deliberately uses only this sanctioned subset of seven, because these are the ones whose
meaning matches what a pondering pass actually does: deliberation_request,
deliberation_result, hypothesis_formed, hypothesis_tested, metacognition_alert,
reasoning_trace_created, workspace_broadcast. Never invent a label — a value outside the
enum fails the insert, and a value inside it but outside this subset misfiles the row.
On start, the bridge queries prior ponder episodes for a similar subject:
SELECT content, actual_outcome, timestamp FROM episodic_memory
WHERE event_type IN ('ponder_converged','ponder_stopped','ponder_start')
AND content ILIKE '%' || $subject || '%'
ORDER BY timestamp DESC LIMIT 5;
and prints them as Prior pondering so the agent starts where it last left off. This is the cross-session self-reflection: the loop remembers having thought about this before.
On judge, it reads open metacognitive_observations for this session’s target layer and
folds their severity into the advice as a tie-breaker toward escalate.
\set or through
psql -v; never string-concatenated into SQL..ponder/<slug>/oca.log and returns null. The bridge may never throw into the
engine.lenses/<name>.md. Front matter plus prose the agent reads 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 covers: what a unit is for this lens, how to enumerate exhaustively, what each dimension means with a concrete 1 / 3 / 5 example, what evidence looks like, and what the four perspectives should each be hunting for.
--lens custom --rubric <file> loads any file with the same front-matter shape.
What the engine guarantees about enumerate. The block is the one part of a lens pack
that an agent is expected to run, so two properties are guaranteed and both are tested:
enumerate: | (or any other | block), # is a
character, not a comment. rg -F '#ff0044' src/, git log --format='%h # %s',
npm run build # rebuilds the token file and a Markdown heading like ## Sweep 2 all
reach the agent whole. Comment stripping applies only to lines being read as YAML
structure — a key/value line, a flow sequence, a block-sequence item — which is why
name: ui # the default lens still yields the name ui.ponder start wraps the
block to terminal width, but a line that looks like a command — quotes, a trailing \,
$VAR or $(…), a pipe, &&, a redirect, a -/-- flag — is printed verbatim, with
the caller’s indent prepended and the line’s own leading whitespace kept. That second
half is load-bearing: a \ continuation is only visibly a continuation while it stays
indented under the command it continues, and the shipped packs and the conformance suite
both depend on it. Runs of internal whitespace are preserved too, so
--format='%h %s' and awk '{ print $1 " " $3 }' are still the program the author
wrote. Prose in the same block does wrap, and a wrapped list item hangs under its text
rather than resetting to the left margin.The width everything is laid out at. COLUMNS wins — it is the shell’s own answer and
the only lever a caller that is not a terminal has. Then the stream’s own width when stdout
is a real TTY. Off a terminal, a fixed 88 columns: output captured into a file still has to
be readable, and 0 or Infinity columns is not a width. The result is clamped to 40–120, so a
maximised window gets a measure rather than a 300-column line. Nothing wraps under --json,
whose output must stay byte-stable, or under PONDER_NO_WRAP=1. A long unbroken token — a
path, a DSN — overflows rather than being cut in half.
So an enumerate block may hold commands of any length, and a custom-lens author does not
have to hand-wrap them or work around the renderer.
coverage < 1.0 blocks stop. No override. This is the reason ponder exists. Deleting
a unit you never graded does not raise coverage (3.5).findings.json or ledger.jsonl.why.psql only.NEXT: line.ponder apply never edits source. It produces a plan, and never out of an aborted session.apply is the separate explicit step.ledger.jsonl before it
touches units.json, findings.json or a pass’s deltas.jsonl, so the only crash state
reachable is a ledger that is ahead of its own view — which ponder verify --repair
rebuilds deterministically (3.6). State ahead of the ledger is unrecoverable and stays a
hard error.judge, decide, report and apply refuse — exit 2 — to draw a conclusion from
units.json or findings.json when they disagree with what replaying ledger.jsonl
says they must contain (3.6). The ledger is checked against the per-pass deltas.jsonl
files too, which are the same records written a second time, so appending one plausible
line to ledger.jsonl does not make a forged state file reconcile. Forging a state file
is detected, not believed. A forgery consistent across all three copies is outside the
model.unit-edit corrects a unit’s label, path, kind and meta;
an attempt to set id is rejected, exit 1. The id is the join key between units and
findings, and moving it would orphan every finding hanging off that unit (3.4).verified-missing (3.5)..git, package.json or another project
marker; an existing .ponder is adopted only when no marker exists anywhere up the tree
(3.1). Every command that resolves a root prints it and says what decided it — silence
about a root is how every path in a session becomes a guess nobody was told about.--project established
the root, a phantom claim whose paths all fail to resolve is refused, exit 1, naming the
root and --project (3.5). “The file is not there” is a claim about a directory; it is
only a claim about the software once you know you are in the right directory.After coverage, minimum passes, thrash and the evidence/review guard, check two-pass convergence before the fallback perseveration stop. Two fully reviewed, confident zero-delta passes earn convergence; stillness with unresolved uncertainty does not.