Get the ODD Loop Status
Answer "where is the loop?" for this repository, from its committed
memory alone. Every input is already in the clone — the stored reports,
the ruling ledgers, and git — so the status costs no backend query, no
running stack, and no network: it is what the loop wrote down about
itself.
Sources — and nothing else
.odd/observe-run-reports/ and .odd/otel-instrumentation-reports/:
frontmatters first, bodies only where a step below needs their
structured tables or a verification's rulings;
- git metadata about the repository and those files: report commit
dates, and each report's
revision and tree_anchor fields against
the commits that came after it — resolved in the repository the
report's repository field names: the store's own when the field is
absent or names the store, otherwise the clone the caller named
(--repository <identity>=<path>), and nowhere when none is named;
.odd/decisions.md, the findings decision ledger, and
.odd/entry-classifications.md, the tree-entry classification
ledger — both read through the ledger contract odd-memory's
decisions reference owns: that reference is the format's
authority, this skill only reads what was written under it. A
missing file means no decision, or no classification, has been
recorded yet, which is a fact, not an error.
All of them are read under the memory contract (odd-memory): frontmatter
first, then sections, never a whole file without a stated need.
The caller may restrict the status to service name(s), a stack (local,
grafana, ...) and/or a deployment environment (prod, uat, ...).
When no filter is named, every stored report qualifies — no arguments is
the whole picture, not an empty scope.
Never query a backend, never start the stack, never write or edit a
report, a ledger, or any other file — this skill reads the loop, it does
not advance it. The one write in the status surface is the ruling
odd-memory's decisions reference records — a decision on a
finding, or the classification of a tree entry, on the user's word.
The status renders in
the conversation — one screen by default, the full tables on request
— never a committed artifact.
Render first, then judge
The build order below is applied by the script bundled with this
skill, in one shell call before any reasoning starts — and once
more, with your rulings as flags, when the first run deferred what
only a judgment can settle:
python3 <this skill's directory>/scripts/odd_status.py --render \
[--service <name>]... [--stack <stack>] [--env <environment>] [--full]
Pass the caller's scope as flags — the service name(s) exactly as
named, the stack, the environment; nothing when the caller named
nothing. The script prints the status as markdown, in one of two
renderings of the same rules:
- The screen — the default, the memory contract's one-screen
synthesis: one line of inventory, one line of memory invariant
(the counts, the violations only), the loop state — one row per
lineage (a service set on a stack and an environment, or a plan on
a stack) with its last report, its burn-down (open,
fixed-and-verified, regressed, declined, unknown) and the next
recommended action, then one line of evidence per lineage under the
table. Then one line saying what the screen dropped, and the
Judgment needed list, ordered by what settling an item changes
— a lineage's boundary first, memory hygiene last — then capped in
length and in count (
+N more), a lineage's item pointing at its
evidence line rather than repeating it.
- The full rendering —
--full, and whenever the caller scoped
the status to a service, a stack or an environment: everything the
screen carries, the inventory and the memory invariant as sections,
plus the working tables whole — the per-service loop state with its
chain and its code boundary, the findings ledger with every finding under its
exact ledger key and its whole title, the trends over the pairs
comparable by construction (a report and the one that verifies
it) and the runs listed apart, the open telemetry gaps as last
recorded, the next recommended action — every row citing its inputs
— and the judgment items whole.
Judgment needed lists everything the rules deferred: a ruling
whose wording states no state or two, verifications that disagree, a
verification stating no verdict, a quick verification that ruled only
part of its items, a boundary the files cannot settle (tree entries
the anchor cannot classify, an entry present on one side only, a
commit-date boundary with commits since), a ruling on an id its chain
does not define (the same finding, or a homonym), a verification that
states a verdict yet keys none of its rulings by a finding of its
chain (with the hint, never the guess, when its keys are the
baseline's ids with an F added or dropped), a quick report's
gaps section opening with its not-queried list, a section not lifted
or cut by a cap, an unreadable report, a malformed frontmatter value,
a skipped ledger row. The action column uses step 6's three actions
plus fix pending (observed, nothing landed, nothing to verify),
plan verified / plan awaits verification for a plan's lineage, and
judgment needed for a deferral.
Rule, then re-run. Rule on the Judgment needed items — those and
nothing else — from the fact sheet (odd_status.py without --render
prints it as JSON; per report, tree_anchor_diff, commits_since,
benchmarks, findings and sections are the keys a judgment reads)
and from a report body only when an item names one; then hand every
ruling back to the script as a flag and run it again:
--ruled <report>/<id>=<state>, <state> one of open, fixed,
regressed: a finding whose ruling the rules could not read ("still
passing" is a pass), a ruling on an id outside its chain you
judge to be the same finding, or the findings a verification left
unread by keying them its own way — the item leaves the list, and the
lineage's action follows, once every finding it names is ruled. A
ruling on a declined finding is refused — the ledger is the memory,
the flag is one run's judgment, never persisted.
--runtime <entry> / --non-runtime <entry> for the top-level
tree entries you can classify, for this run only — a classification
that holds for the repository is recorded once in
.odd/entry-classifications.md through odd-memory's decisions
reference, on the user's word and never on your own, and the script
reads it before the built-in list on every later run; a flag
overrides both for one run and never persists. When an entry is
runtime for one lineage only, leave the deferral and say so.
--repository <identity>=<path>, repeatable, for a report naming
another repository than the store's: where that repository is
cloned, the identity as the report's repository field writes it —
or the clone's own git remote get-url origin, which the script
normalizes the same way, when the caller gives only a path. From
the caller, or from where you cloned or found the repository in
this session — never guessed: without a pair the boundary stays
unknown, which is a fact to state, and the reply asks for it.
--today YYYY-MM-DD sets the date the cadence rule counts from;
--help lists the rest.
The judgments are applied before the rendering, never after it: one
burn-down, one truth. What no flag can carry stays in the list, and
stays deferred.
Print the last rendering in the conversation as the status,
unchanged, and say which flags it ran with. The tables are the
rules; never rewrite a rendered row: when the sources contradict one,
flag the row in the paragraph below the rendering, with the evidence.
Then, under the rendering, the synthesis — in the conversation's
language, three sentences at most: where the loop is healthy, what is
due, the next command to run — every claim taken from the rendering
above, nothing from memory. The reply is the synthesis; the tables
are the working data. What is still deferred after the re-run is
stated as what it would change if settled, and nothing when it
changes nothing. Runs listed apart in the trends are information, not
a deferral: compare them only when the caller asks.
When python3 is missing, the script is not next to this file (an
install that dropped scripts/), or it exits non-zero, say so in one
line and build the status by hand, exactly as the steps below describe
and in the screen's shape unless the caller asked for the full tables
— the script is the same rules applied by code, never a different
status.
Build the status in this order
- Inventory — frontmatters only. List both directories and read
every frontmatter, no bodies yet; the memory invariant (below) is
checked here, over every stored report, and rendered right after
the inventory. No
.odd/ directory or no reports
at all: say the loop has not started here, point at
/odd-instrument-otel or /odd-observe, and stop — that IS the
status, not a failure.
- Per-service loop state. One row per lineage — a service set
(
services for observation reports; an instrumentation report
contributes to the services its plan covers, project names its
scope) on a stack and an environment. The repository a report names
is never part of that key — the field arriving on a later report
must not split a chain — but the row's evidence states it whenever
the last report names another repository than the store's, or the
store holds reports of several. Per lineage: last
observation (date, stack, environment, mode, depth — full
when the frontmatter has none — workload when present), last
verification (mode: verify reports — their verifies value names
what they replayed) with its verdict from the report body — a
depth: quick verification renders its coverage
(PASS (quick, 3 of 5 ruled)) and satisfies "verified" only for
the items it ruled, never for the service as a whole; a
verification's presence rulings satisfy "verified" only for the
items ruled closed, present, unattributed closes nothing — and
the chain as the files tell it:
observed -> fixed -> verified. A mode: re-measure report is an
observation event, never a verification: it replayed the protocol of
the report its verifies names without ruling on a fix — count it as
the last observation when newest, and never let it satisfy "verified"
in the chain.
"Fixed" means commits landed after the report's revision,
excluding commits that only touch the loop's own memory or
documentation — the memory being the append-only report stores
and the ledger (.odd/observe-run-reports/,
.odd/otel-instrumentation-reports/, .odd/decisions.md), never
.odd/benchmarks/: a benchmark is living source, and a commit
changing one is a fix like any other. Scope the commit test to the
service's path when a report names one (an instrumentation report's
project); otherwise say the count is repo-wide, not service-scoped.
A verification covers the commits its own revision has as ancestors
— but in a squash-merge workflow that revision never becomes an
ancestor of the merged history, so ancestry alone cannot prove
coverage: a commit whose squash introduced the verification report
itself is covered by that verification, and when ancestry is
otherwise inconclusive, say coverage is uncertain rather than ruling
a verification due. When a report carries a tree_anchor, that is
the preferred boundary: compare its entry hashes against
git ls-tree of the candidate commit, ignoring .odd (its
top-level hash moves with every report written) and every entry
that cannot change the observed service's runtime behavior —
documentation is the canonical case, but so are CI configuration,
generated/packaging artifacts, and release-metadata files; the
repository's own rulings in .odd/entry-classifications.md come
before the built-in list of such names, and a flag given for one
run before both; a ruling settles an entry whose hash differs, and
an entry present on one side only (added or removed since the
anchor) stays uncertain whatever its ruling. Then test the
benchmark by path: the one the report's scenario record
names (.odd/benchmarks/<name>/) — nothing when it names none, a
benchmark the run did not use cannot be its fix; commits touching
that path since the report (git log <revision>..HEAD -- <path>
when git rev-parse --verify <revision>^{commit} succeeds,
otherwise git log --since=<the report file's own commit date> -- <path>, the report's own commit ignored). Equal hashes and no
benchmark
commit mean no code change, and the comparison resolves in any
clone whatever the merge strategy; when the only differing entries
are ones you cannot classify, the boundary is uncertain — say so,
never rule "code changed". When a report carries neither an anchor
nor a revision, its commit date — already a source — is the
substitute boundary. A report naming another repository resolves
its revision and anchor in that repository's clone, the one the
caller named; when none is named, when the report spans
repositories (a per-service map with several values), or when its
value is not a remote the rules can read, the boundary is
unknown — judgment needed, never "no code change", and never
the report's commit date, a fact about the store rather than the
service. A benchmark lives in the store, so its commits are counted
there from the report's own commit date when the report's revision
belongs to a clone. The classification ledger holds for every
repository the store holds reports of: a row may name an entry
present only in a clone the caller named, and a store ruling on
src applies to a clone's src too — say so when that reading is
doubtful. Never echo a filesystem path from the fact sheet into a
stored report or a ledger row.
Pre-convention reports (no verifies field) leave the chain
"unknown (pre-convention)" — state it, never reconstruct it from
prose.
- Findings ledger. From each report's ranked findings table, each
verification's rulings, and the decisions ledger: open,
fixed-and-verified, regressed, or declined, with severity — the
burn-down of the loop's backlog. A finding no verification ever ruled
on stays open, whatever a commit message claims —
not ruled (quick)
in a quick verification is not a ruling — unless the
decisions ledger declines it.
Cross-reference every finding against .odd/decisions.md on the key
that ledger uses: <exact report filename> / <finding ID as the report's ranked-findings table names it> — the exact filename is
what disambiguates finding IDs, which are report-local (two reports
can both have an "F4"). Rows are appended, never rewritten, so the
latest row for a finding wins: when it carries a declining verdict
(wontfix, or any verdict other than open) the finding renders
declined — its verdict, decision date, and rationale in place of
the open state — and when it carries open the finding is reopened,
back to whatever the reports rule. A finding with no ledger row
renders exactly as it would without the ledger.
Count open, fixed-and-verified, regressed, and declined separately
in the burn-down: a declined finding was decided, not fixed, and
merging the two would hide the backlog's real shape.
- Trends. For operations appearing in the per-operation summary
table of two or more reports of the same service, stack, environment
and
workload: p50/p95/p99 and error rate across runs — improved /
regressed / stable, with the stored numbers. Depth does not break
comparability: section 2's numbers come from the same source at
both depths, so a quick and a full run of the same scenario compare
on the operations both carry. Comparability is
stricter than the frontmatter: reports whose workload differs are
incomparable, and for drive-mode reports (and verifications or
re-measures replaying one) so are runs whose recorded scenario or
process identity (instance, process_restarted) differ — a driven
session and a process-per-call run measure different things whatever
the frontmatter says. A verification or re-measure and the report its
verifies names replay the same scenario by construction and always
compare — unless the benchmark the drive replays moved between them,
which the verification's scenario record states: then only what the
record says still compares (the load unchanged) does, and the rest
is listed apart. List incomparable runs apart, never diff them.
Stored numbers only — no live queries.
- Open telemetry gaps. Gaps recorded in report bodies and not
closed by a later verification ruling or instrumentation report —
closed is the only closing ruling; a planned item ruled
present, unattributed stays open. A
quick report's not queried (quick): ... list is a statement about
that mission, never a gap — do not count it; and a gap a quick
verification lists as not ruled (quick) stays open, not closed.
When gaps dominate a service's picture, the recommendation below
should say instrument, not observe.
- Next recommended action — the maturity principle operationalized,
per service: a verification is due (service-relevant commits —
step 2's rule — landed after the last report's
revision and no
verification covers them), a new observation is due or overdue
(the cadence of past observation dates has lapsed, or recent verdicts
keep churning), or the loop can rest (recent verification, stable
verdicts, no unverified change) — or the boundary is unknown
(judgment needed: the report's repository is not reachable, or
the report spans repositories) and the reply says what would
settle it. Every recommendation cites its inputs — dates, verdicts,
revisions — evidence over impressions applies to the meta-loop
too.
The memory invariant
"Model-visible means logged": everything a later mission consumes is
in .odd/, in the shape the odd-memory contract fixes — and the
script checks it after the fact, over every stored report and
decision, filtered status or not. Per report: the filename convention,
the frontmatter fields the kind requires (services, stack,
environment, mode, depth, window as start/end UTC,
run_name and date matching the filename; project for a plan),
and a verifies that names a stored file when the mode is a replay.
Per decision row: a report that exists and a finding it carries; per
classification row: a top-level entry of HEAD and a class the rule
knows (each ledger's own skipped rows). The fact sheet carries the
result under
invariant; the full rendering carries a ## Memory invariant
section — the counts, then one line per violation — and the screen
carries the same as one line, violations only.
A violation is never a failure: the store is append-only, so a
report is never edited to repair it — a new run supersedes it — and a
ruling row is appended, never rewritten. The status is where a
reader learns that a decision points at nothing, or a classification
at no entry; the remedy is the next run, or a new row. A report whose
only gap is a field it predates (depth, read as full the way the
loop state already renders it) is not a violation: the fact sheet
lists it under
legacy, and the section names it in a note next to the counts,
since nothing can ever change it.
A filter that matches nothing is still a status
When the caller restricted the status — service, stack, environment —
and no stored report matches, say exactly two things and stop:
- what was searched: each filter and its value, kept distinct (a
stack scope is not an environment scope);
- what exists instead: the distinct services, stacks, and
environments present across the stored frontmatters, and the
repositories when any report names one — the inventory of step 1,
which is already read.
Example: "no report with environment prod — all 4 stored reports are
environment: local." That IS the status, not a failure: it names the
miss and hands back the values that let the caller correct the scope in
one turn.
Service names match exactly against the frontmatter services
values (and an instrumentation report's plan scope): a partial name
misses, and falls into the statement above rather than being guessed
into a match. Never render the unfiltered picture under a scope that
matched nothing, never render an empty table silently, and never error.
Degrade gracefully
Degrade gracefully everywhere: a single report, reports predating newer
frontmatter fields, a body missing a structured section — render what
exists, mark what cannot be known ("no verification yet", "chain
unknown"), and never fail the whole status over one unreadable report.
The ledger degrades the same way: a malformed row, or one naming a
report or finding ID that does not exist, is reported and skipped —
say which row and why, then render the rest of the status. A broken
ledger row is never fatal, and never silently dropped either.
1---2name: get-status3description: Render the state of the ODD loop from the repository's committed .odd/ history and git alone - one screen by default (the loop state per lineage with its burn-down and next action), the full tables on request (per-service loop state, findings ledger, trends, open telemetry gaps) - read-only, no backend queries, no report written. Use when answering where the loop is, when /odd-status runs, or when a status must be computed offline from the clone.4---56# Get the ODD Loop Status78Answer "where is the loop?" for this repository, from its committed9memory alone. Every input is already in the clone — the stored reports,10the ruling ledgers, and git — so the status costs no backend query, no11running stack, and no network: it is what the loop wrote down about12itself.1314## Sources — and nothing else1516- `.odd/observe-run-reports/` and `.odd/otel-instrumentation-reports/`:17 frontmatters first, bodies only where a step below needs their18 structured tables or a verification's rulings;19- git metadata about the repository and those files: report commit20 dates, and each report's `revision` and `tree_anchor` fields against21 the commits that came after it — resolved in the repository the22 report's `repository` field names: the store's own when the field is23 absent or names the store, otherwise the clone the caller named24 (`--repository <identity>=<path>`), and nowhere when none is named;25- `.odd/decisions.md`, the findings decision ledger, and26 `.odd/entry-classifications.md`, the tree-entry classification27 ledger — both read through the ledger contract `odd-memory`'s28 `decisions` reference owns: that reference is the format's29 authority, this skill only reads what was written under it. A30 missing file means no decision, or no classification, has been31 recorded yet, which is a fact, not an error.3233All of them are read under the memory contract (`odd-memory`): frontmatter34first, then sections, never a whole file without a stated need.3536The caller may restrict the status to service name(s), a stack (`local`,37`grafana`, ...) and/or a deployment environment (`prod`, `uat`, ...).38When no filter is named, every stored report qualifies — no arguments is39the whole picture, not an empty scope.4041Never query a backend, never start the stack, never write or edit a42report, a ledger, or any other file — this skill reads the loop, it does43not advance it. The one write in the status surface is the ruling44`odd-memory`'s `decisions` reference records — a decision on a45finding, or the classification of a tree entry, on the user's word.46The status renders in47the conversation — one screen by default, the full tables on request48— never a committed artifact.4950## Render first, then judge5152The build order below is applied by the script bundled with this53skill, in **one shell call** before any reasoning starts — and once54more, with your rulings as flags, when the first run deferred what55only a judgment can settle:5657```bash58python3 <this skill's directory>/scripts/odd_status.py --render \59 [--service <name>]... [--stack <stack>] [--env <environment>] [--full]60```6162Pass the caller's scope as flags — the service name(s) exactly as63named, the stack, the environment; nothing when the caller named64nothing. The script prints the status as markdown, in one of two65renderings of the same rules:6667- **The screen** — the default, the memory contract's one-screen68 synthesis: one line of inventory, one line of memory invariant69 (the counts, the violations only), the **loop state** — one row per70 lineage (a service set on a stack and an environment, or a plan on71 a stack) with its last report, its burn-down (open,72 fixed-and-verified, regressed, declined, unknown) and the next73 recommended action, then one line of evidence per lineage under the74 table. Then one line saying what the screen dropped, and the75 **Judgment needed** list, ordered by what settling an item changes76 — a lineage's boundary first, memory hygiene last — then capped in77 length and in count (`+N more`), a lineage's item pointing at its78 evidence line rather than repeating it.79- **The full rendering** — `--full`, and whenever the caller scoped80 the status to a service, a stack or an environment: everything the81 screen carries, the inventory and the memory invariant as sections,82 plus the working tables whole — the per-service loop state with its83 chain and its code boundary, the findings ledger with every finding under its84 exact ledger key and its whole title, the trends over the pairs85 comparable by construction (a report and the one that `verifies`86 it) and the runs listed apart, the open telemetry gaps as last87 recorded, the next recommended action — every row citing its inputs88 — and the judgment items whole.8990**Judgment needed** lists everything the rules deferred: a ruling91whose wording states no state or two, verifications that disagree, a92verification stating no verdict, a quick verification that ruled only93part of its items, a boundary the files cannot settle (tree entries94the anchor cannot classify, an entry present on one side only, a95commit-date boundary with commits since), a ruling on an id its chain96does not define (the same finding, or a homonym), a verification that97states a verdict yet keys none of its rulings by a finding of its98chain (with the hint, never the guess, when its keys are the99baseline's ids with an `F` added or dropped), a quick report's100gaps section opening with its not-queried list, a section not lifted101or cut by a cap, an unreadable report, a malformed frontmatter value,102a skipped ledger row. The action column uses step 6's three actions103plus `fix pending` (observed, nothing landed, nothing to verify),104`plan verified` / `plan awaits verification` for a plan's lineage, and105`judgment needed` for a deferral.106107**Rule, then re-run.** Rule on the Judgment needed items — those and108nothing else — from the fact sheet (`odd_status.py` without `--render`109prints it as JSON; per report, `tree_anchor_diff`, `commits_since`,110`benchmarks`, `findings` and `sections` are the keys a judgment reads)111and from a report body only when an item names one; then hand every112ruling back to the script as a flag and run it again:113114- `--ruled <report>/<id>=<state>`, `<state>` one of `open`, `fixed`,115 `regressed`: a finding whose ruling the rules could not read ("still116 passing" is a pass), a ruling on an id outside its chain you117 judge to be the same finding, or the findings a verification left118 unread by keying them its own way — the item leaves the list, and the119 lineage's action follows, once every finding it names is ruled. A120 ruling on a declined finding is refused — the ledger is the memory,121 the flag is one run's judgment, never persisted.122- `--runtime <entry>` / `--non-runtime <entry>` for the top-level123 tree entries you can classify, for this run only — a classification124 that holds for the repository is recorded once in125 `.odd/entry-classifications.md` through `odd-memory`'s `decisions`126 reference, on the user's word and never on your own, and the script127 reads it before the built-in list on every later run; a flag128 overrides both for one run and never persists. When an entry is129 runtime for one lineage only, leave the deferral and say so.130- `--repository <identity>=<path>`, repeatable, for a report naming131 another repository than the store's: where that repository is132 cloned, the identity as the report's `repository` field writes it —133 or the clone's own `git remote get-url origin`, which the script134 normalizes the same way, when the caller gives only a path. From135 the caller, or from where you cloned or found the repository in136 this session — never guessed: without a pair the boundary stays137 unknown, which is a fact to state, and the reply asks for it.138- `--today YYYY-MM-DD` sets the date the cadence rule counts from;139 `--help` lists the rest.140141The judgments are applied before the rendering, never after it: one142burn-down, one truth. What no flag can carry stays in the list, and143stays deferred.144145**Print the last rendering in the conversation as the status,146unchanged**, and say which flags it ran with. The tables are the147rules; never rewrite a rendered row: when the sources contradict one,148flag the row in the paragraph below the rendering, with the evidence.149Then, under the rendering, the **synthesis** — in the conversation's150language, three sentences at most: where the loop is healthy, what is151due, the next command to run — every claim taken from the rendering152above, nothing from memory. The reply is the synthesis; the tables153are the working data. What is still deferred after the re-run is154stated as what it would change if settled, and nothing when it155changes nothing. Runs listed apart in the trends are information, not156a deferral: compare them only when the caller asks.157158When `python3` is missing, the script is not next to this file (an159install that dropped `scripts/`), or it exits non-zero, say so in one160line and build the status by hand, exactly as the steps below describe161and in the screen's shape unless the caller asked for the full tables162— the script is the same rules applied by code, never a different163status.164165## Build the status in this order1661671. **Inventory — frontmatters only.** List both directories and read168 every frontmatter, no bodies yet; the memory invariant (below) is169 checked here, over every stored report, and rendered right after170 the inventory. No `.odd/` directory or no reports171 at all: say the loop has not started here, point at172 `/odd-instrument-otel` or `/odd-observe`, and stop — that IS the173 status, not a failure.1742. **Per-service loop state.** One row per lineage — a service set175 (`services` for observation reports; an instrumentation report176 contributes to the services its plan covers, `project` names its177 scope) on a stack and an environment. The repository a report names178 is never part of that key — the field arriving on a later report179 must not split a chain — but the row's evidence states it whenever180 the last report names another repository than the store's, or the181 store holds reports of several. Per lineage: last182 observation (date, `stack`, `environment`, mode, `depth` — `full`183 when the frontmatter has none — `workload` when present), last184 verification (`mode: verify` reports — their `verifies` value names185 what they replayed) with its verdict from the report body — a186 `depth: quick` verification renders its coverage187 (`PASS (quick, 3 of 5 ruled)`) and satisfies "verified" only for188 the items it ruled, never for the service as a whole; a189 verification's presence rulings satisfy "verified" only for the190 items ruled `closed`, `present, unattributed` closes nothing — and191 the chain as the files tell it:192 observed -> fixed -> verified. A `mode: re-measure` report is an193 observation event, never a verification: it replayed the protocol of194 the report its `verifies` names without ruling on a fix — count it as195 the last observation when newest, and never let it satisfy "verified"196 in the chain.197 "Fixed" means commits landed after the report's `revision`,198 **excluding commits that only touch the loop's own memory or199 documentation** — the memory being the append-only report stores200 and the ledger (`.odd/observe-run-reports/`,201 `.odd/otel-instrumentation-reports/`, `.odd/decisions.md`), never202 `.odd/benchmarks/`: a benchmark is living source, and a commit203 changing one is a fix like any other. Scope the commit test to the204 service's path when a report names one (an instrumentation report's205 `project`); otherwise say the count is repo-wide, not service-scoped.206 A verification covers the commits its own `revision` has as ancestors207 — but in a squash-merge workflow that `revision` never becomes an208 ancestor of the merged history, so ancestry alone cannot prove209 coverage: a commit whose squash introduced the verification report210 itself is covered by that verification, and when ancestry is211 otherwise inconclusive, say coverage is uncertain rather than ruling212 a verification due. When a report carries a `tree_anchor`, that is213 the **preferred boundary**: compare its entry hashes against214 `git ls-tree` of the candidate commit, ignoring `.odd` (its215 top-level hash moves with every report written) and every entry216 that cannot change the observed service's runtime behavior —217 documentation is the canonical case, but so are CI configuration,218 generated/packaging artifacts, and release-metadata files; the219 repository's own rulings in `.odd/entry-classifications.md` come220 before the built-in list of such names, and a flag given for one221 run before both; a ruling settles an entry whose hash differs, and222 an entry present on one side only (added or removed since the223 anchor) stays uncertain whatever its ruling. Then test the224 benchmark by path: the one the report's scenario record225 names (`.odd/benchmarks/<name>/`) — nothing when it names none, a226 benchmark the run did not use cannot be its fix; commits touching227 that path since the report (`git log <revision>..HEAD -- <path>`228 when `git rev-parse --verify <revision>^{commit}` succeeds,229 otherwise `git log --since=<the report file's own commit date> --230 <path>`, the report's own commit ignored). Equal hashes and no231 benchmark232 commit mean no code change, and the comparison resolves in any233 clone whatever the merge strategy; when the only differing entries234 are ones you cannot classify, the boundary is uncertain — say so,235 never rule "code changed". When a report carries neither an anchor236 nor a `revision`, its commit date — already a source — is the237 substitute boundary. A report naming another repository resolves238 its `revision` and anchor in that repository's clone, the one the239 caller named; when none is named, when the report spans240 repositories (a per-service map with several values), or when its241 value is not a remote the rules can read, the boundary is242 **unknown** — `judgment needed`, never "no code change", and never243 the report's commit date, a fact about the store rather than the244 service. A benchmark lives in the store, so its commits are counted245 there from the report's own commit date when the report's revision246 belongs to a clone. The classification ledger holds for every247 repository the store holds reports of: a row may name an entry248 present only in a clone the caller named, and a store ruling on249 `src` applies to a clone's `src` too — say so when that reading is250 doubtful. Never echo a filesystem path from the fact sheet into a251 stored report or a ledger row.252 Pre-convention reports (no `verifies` field) leave the chain253 "unknown (pre-convention)" — state it, never reconstruct it from254 prose.2553. **Findings ledger.** From each report's ranked findings table, each256 verification's rulings, and the decisions ledger: open,257 fixed-and-verified, regressed, or declined, with severity — the258 burn-down of the loop's backlog. A finding no verification ever ruled259 on stays open, whatever a commit message claims — `not ruled (quick)`260 in a quick verification is not a ruling — unless the261 decisions ledger declines it.262 Cross-reference every finding against `.odd/decisions.md` on the key263 that ledger uses: `<exact report filename> / <finding ID as the264 report's ranked-findings table names it>` — the exact filename is265 what disambiguates finding IDs, which are report-local (two reports266 can both have an "F4"). Rows are appended, never rewritten, so **the267 latest row for a finding wins**: when it carries a declining verdict268 (`wontfix`, or any verdict other than `open`) the finding renders269 **declined** — its verdict, decision date, and rationale in place of270 the open state — and when it carries `open` the finding is reopened,271 back to whatever the reports rule. A finding with no ledger row272 renders exactly as it would without the ledger.273 Count open, fixed-and-verified, regressed, and declined **separately**274 in the burn-down: a declined finding was decided, not fixed, and275 merging the two would hide the backlog's real shape.2764. **Trends.** For operations appearing in the per-operation summary277 table of two or more reports of the same service, stack, environment278 and `workload`: p50/p95/p99 and error rate across runs — improved /279 regressed / stable, with the stored numbers. Depth does not break280 comparability: section 2's numbers come from the same source at281 both depths, so a quick and a full run of the same scenario compare282 on the operations both carry. Comparability is283 stricter than the frontmatter: reports whose `workload` differs are284 incomparable, and for drive-mode reports (and verifications or285 re-measures replaying one) so are runs whose recorded scenario or286 process identity (`instance`, `process_restarted`) differ — a driven287 session and a process-per-call run measure different things whatever288 the frontmatter says. A verification or re-measure and the report its289 `verifies` names replay the same scenario by construction and always290 compare — unless the benchmark the drive replays moved between them,291 which the verification's scenario record states: then only what the292 record says still compares (the load unchanged) does, and the rest293 is listed apart. List incomparable runs apart, never diff them.294 Stored numbers only — no live queries.2955. **Open telemetry gaps.** Gaps recorded in report bodies and not296 closed by a later verification ruling or instrumentation report —297 `closed` is the only closing ruling; a planned item ruled298 `present, unattributed` stays open. A299 quick report's `not queried (quick): ...` list is a statement about300 that mission, never a gap — do not count it; and a gap a quick301 verification lists as `not ruled (quick)` stays open, not closed.302 When gaps dominate a service's picture, the recommendation below303 should say instrument, not observe.3046. **Next recommended action** — the maturity principle operationalized,305 per service: a **verification is due** (service-relevant commits —306 step 2's rule — landed after the last report's `revision` and no307 verification covers them), a **new observation is due or overdue**308 (the cadence of past observation dates has lapsed, or recent verdicts309 keep churning), or the **loop can rest** (recent verification, stable310 verdicts, no unverified change) — or the **boundary is unknown**311 (`judgment needed`: the report's repository is not reachable, or312 the report spans repositories) and the reply says what would313 settle it. Every recommendation cites its inputs — dates, verdicts,314 revisions — evidence over impressions applies to the meta-loop315 too.316317## The memory invariant318319"Model-visible means logged": everything a later mission consumes is320in `.odd/`, in the shape the `odd-memory` contract fixes — and the321script checks it after the fact, over **every** stored report and322decision, filtered status or not. Per report: the filename convention,323the frontmatter fields the kind requires (`services`, `stack`,324`environment`, `mode`, `depth`, `window` as `start/end` UTC,325`run_name` and `date` matching the filename; `project` for a plan),326and a `verifies` that names a stored file when the mode is a replay.327Per decision row: a report that exists and a finding it carries; per328classification row: a top-level entry of HEAD and a class the rule329knows (each ledger's own skipped rows). The fact sheet carries the330result under331`invariant`; the full rendering carries a `## Memory invariant`332section — the counts, then one line per violation — and the screen333carries the same as one line, violations only.334335A violation is **never a failure**: the store is append-only, so a336report is never edited to repair it — a new run supersedes it — and a337ruling row is appended, never rewritten. The status is where a338reader learns that a decision points at nothing, or a classification339at no entry; the remedy is the next run, or a new row. A report whose340only gap is a field it predates (`depth`, read as `full` the way the341loop state already renders it) is not a violation: the fact sheet342lists it under343`legacy`, and the section names it in a note next to the counts,344since nothing can ever change it.345346## A filter that matches nothing is still a status347348When the caller restricted the status — service, stack, environment —349and no stored report matches, say exactly two things and stop:350351- **what was searched**: each filter and its value, kept distinct (a352 stack scope is not an environment scope);353- **what exists instead**: the distinct services, stacks, and354 environments present across the stored frontmatters, and the355 repositories when any report names one — the inventory of step 1,356 which is already read.357358Example: "no report with environment `prod` — all 4 stored reports are359`environment: local`." That IS the status, not a failure: it names the360miss and hands back the values that let the caller correct the scope in361one turn.362363Service names match **exactly** against the frontmatter `services`364values (and an instrumentation report's plan scope): a partial name365misses, and falls into the statement above rather than being guessed366into a match. Never render the unfiltered picture under a scope that367matched nothing, never render an empty table silently, and never error.368369## Degrade gracefully370371Degrade gracefully everywhere: a single report, reports predating newer372frontmatter fields, a body missing a structured section — render what373exists, mark what cannot be known ("no verification yet", "chain374unknown"), and never fail the whole status over one unreadable report.375The ledger degrades the same way: a malformed row, or one naming a376report or finding ID that does not exist, is **reported and skipped** —377say which row and why, then render the rest of the status. A broken378ledger row is never fatal, and never silently dropped either.