The .odd/ memory contract
The ODD loop's memory is a set of committed files in the observed
repository, under .odd/: git versions them, pull requests review them,
every user of the repository shares them — no side-channel storage,
nothing opaque. It has five kinds, each with the reference that owns
its specifics (its paths, its frontmatter fields, its recall matching,
its branch name, its commit subject, what its reply carries, and how a
stored one is shown):
| Kind |
Store |
Reference |
| Observation reports |
.odd/observe-run-reports/ |
references/observe-run-report.md |
| Instrumentation reports |
.odd/otel-instrumentation-reports/ |
references/otel-instrumentation-report.md |
| Maintainer rulings |
.odd/decisions.md (findings) and .odd/entry-classifications.md (tree entries), written by scripts/odd_ledger.py |
references/decisions.md |
| Benchmarks |
.odd/benchmarks/<name>/ |
references/benchmark.md |
| Custom stacks |
.odd/observability-stacks/<name>.md |
references/observability-stack.md |
This file owns what they share; a reference states only its
specifics. A sixth kind is a new row in this table and a new
reference, never a sixth copy of these rules.
Reading a reference by section
A reference is read like a stack reference — by the section a step
needs, never whole. To persist an artifact, read its naming and
format sections and its ## Rules; to recall the baseline before
a run, its ## Recall; to show a stored artifact at the end of a
mission, its ## Show and nothing else; to record a decision, the
ledger reference's ## Recording a decision and ## Rules, and to
record a tree-entry classification, its
## The entry-classification ledger. The ## Return value of a
report reference is what the persistence hands the caller, and what
## Show renders from.
Where the memory lives
- In the observed (or investigated) repository, exactly where the
kind's reference says — never in the oddyssey package, a home directory,
or a temp path. Create the directory when it does not exist.
- Committed: the files stay tracked, never added to
.gitignore.
- A report file is named
YYYY-MM-DD-HHmm-<run_name>.md: the run's
UTC start time to the minute, so two same-day runs never collide
and a plain listing sorts chronologically — computed with date -u,
never from the local clock (a session crossing local midnight while
UTC has not names the wrong day) — then a short kebab-case slug
naming the content, not the date. The kind's reference adds its own
prefixes and variants.
The frontmatter and the body
- A report opens with a YAML frontmatter so later runs can filter the
store without parsing prose. Every field mirrors the run as it
actually executed — defaults applied, not as requested. The kind's
reference lists the fields and their meaning.
- The body is the producing agent's judgment as-is, whole: what
each section carries is the agent's contract, stated once where the
kind's reference says — in that reference, beside the format, or in
the agent — and read at report time; the persistence adds nothing to
it beyond the numbered headings and the few machine-read shapes a
kind's script fixes and checks (its reference names them), and a
summary cannot feed a later diff.
Append-only, with one exception
- The report stores and the ruling ledgers are append-only
evidence: one run, one file — never edit a stored report to
"update" it, a new run writes a new file and the diff lives there; a
decision is a row appended, never rewritten, and the latest row for a
finding wins, as does the latest row for a tree entry. A report is
never modified to carry a decision: the ledger is the decision's only
home — and a ledger is written by the script its reference names,
which checks the row before it lands, never by a file tool; the
reference's prose steps are the fallback where the script cannot
run.
- A benchmark is living source, not a run record: it is updated in
place through reviewed diffs, and git history, not file accumulation,
is its memory. It is never overwritten silently — an update is a diff
the maintainer reviews like any other committed change. A custom
stack file is living source the same way.
- The consumers keep the two apart:
/odd-verify and /odd-status
treat a commit that touches only the report stores or a ledger as
memory, not code, while a commit that changes a benchmark or a custom
stack file is a code change.
Recall: reading the memory
For the two report stores and the benchmarks — a custom stack is
recalled by name, each ledger is one file; their references own that:
- Run the recall script this skill carries,
python3 <this skill's directory>/scripts/odd_recall.py --repo <path>
with the mission's scope as flags — the kind's reference says which.
The invocation may travel with the mission: a caller's preflight
that already resolved the scope writes it into the mission block as
Recall: python3 <this skill's directory>/scripts/odd_recall.py --repo <path> --service <name> [--service <name> ...] --stack <stack> --depth <depth>
(the observation form — one --service per name, --env left for
the agent to append once it has detected the environment; a benchmark
is recalled by --kind benchmark --service <name> alone, an
instrumentation report by --kind instrumentation --project <scope>),
and the agent runs that line as is — never re-derived, never a flag
guessed. The whole surface is --repo, --kind <observation | instrumentation | benchmark> (default observation), --service,
--stack, --env, --depth, --mode (repeatable) and --project
— nothing else: --help has nothing to add and the file has nothing
to read. The script reads every stored frontmatter (a benchmark's manifest) in
Python and prints the matches newest first, one line each; a
report the frontmatter contract flags, or a benchmark whose manifest
it cannot read, is listed and reported, never skipped silently. No
frontmatter reaches the conversation until the baseline is chosen.
A missing or empty store is a first run — the script says so, never
fails. By hand, only when the script cannot run: for a report
store, list it newest first (the filenames sort chronologically) and
read frontmatter blocks only, never whole files; the benchmarks
are a directory store, so list the directories and read the few
manifest keys their reference names, never a whole manifest. Either
way apply the kind's matching rules yourself; a missing or empty
store is a first run then too — say so, never fail.
- The first line printed is the baseline: read it by section, never
whole — the kind's reference names the sections a mission needs,
and the command that prints them when it ships one.
Reading beyond that set is the exception, for a stated need that the
calling agent records. A benchmark recall has no baseline: the whole
listing is the set the mission checks itself against, and its
reference says what each column answers.
- Older matches are history: read them only when a trend or the
evolution of one decision matters, and only the sections in question.
No secrets, no real identifiers
Every file of the memory is made to be committed and shared. Never
write a token, credential, cookie, or connection string into one —
refer to access material by variable or secret name only. The same for
real identifiers that carry no access on their own: tenant,
workspace, subscription, resource-group or site names and GUIDs,
account or login names, home-directory paths — anything that identifies
a real customer, tenant or environment — and every value persisted
under a remote stack's stack_config, regions and the local stack
excepted: a log group or a profile name reads like a plain path and
identifies an account all the same; write the field's name in angle
brackets instead (<log_group>). A live CLI excerpt, a
configuration display, or a mission block's preflight handoff is the
likeliest source: every such value lands in the file as an obviously
fake placeholder (Contoso, a zeroed or patterned GUID,
example-user, <scratchpad>). On a host that runs the package's
lifecycle hooks, a hook flags what slipped through, after the write.
The work branch and the lone commit
An observation report's persist (its reference's script) applies this
section; everywhere else it is applied by hand:
- Never commit on the default branch. Before committing, compare
git branch --show-current with the repository's default branch
(git symbolic-ref --short refs/remotes/origin/HEAD stripped of its
origin/ prefix; if unset, main — or master when that is the
checked-out branch). Only when on the default branch, create and
switch to the work branch the kind's reference names (switching to it if
it already exists), commit there, and say so in the reply. If
switching is impossible, do not commit: state the path and leave the
commit to the caller. On a host that runs the package's lifecycle
hooks, a hook refuses the commit itself; this rule stays the
enforcement everywhere else.
- Commit the artifact's files alone:
git add the files the
persistence just wrote, then git commit with the subject the
kind's reference gives (docs(odd): ...) — never stage anything else; a
dirty working tree stays untouched otherwise. If committing is
impossible (not a git repository, or the caller said not to), state
the path and leave the commit to the caller.
- Either way the reply states the stored path and the carrying commit
(
git rev-parse --short HEAD right after the commit), or
not committed with the reason.
The reply and the synthesis
- The persistence's return value carries the stored path, the
carrying commit, and what its
## Show needs to render — the
synthesis inputs, quoted from the artifact where the artifact
carries the value, never rephrased, the kind's reference lists them;
or, when the kind ships a script that renders the synthesis from the
stored file, the headline alone, so the block never travels through
the reply — and never the artifact's body: an observation report
runs 300 to 500 lines, the reply travels back into the caller's
context, and the synthesis is its only reader. What the next wave
needs is in the file, at the stored path.
## Show renders from that return value, or reads a stored
artifact the caller names from disk, by section, with its carrying
commit from git (git log -1 --format=%h -- <path>) — never from the
conversation's memory of the mission.
- Everything in the synthesis comes from the stored artifact: no
backend query, no doc fetch, no re-derivation, no invented value; the
carrying commit is the one value outside the file, and a value the
artifact does not carry is absent from the synthesis too.
- One screen, hard cap: prefer dropping rows behind a
+N more in the report marker over growing sections.
- The synthesis renders in the conversation's language; the stored
artifact itself stays English.
- The synthesis never replaces the artifact: the next wave consumes
the stored file — state its path, never re-inline the full body.
1---2name: odd-memory3description: The .odd/ memory - its contract and one reference per kind. The contract states what every kind shares (where the memory lives, the frontmatter and the whole body, append-only reports versus living-source benchmarks, recall by frontmatter then by section, the no-secrets rule, the work branch and the lone commit, the reply that carries a synthesis and never the artifact); each reference says how to persist, recall and show its kind - observation reports, instrumentation reports, benchmarks, the maintainer-ruling ledgers (finding decisions, tree-entry classifications), custom stack files. Read when a report, a benchmark, a custom stack file or a decision is persisted, recalled, shown or inventoried, or when a finding is declined (wontfix) or such a decision reversed, or a tree entry is ruled runtime or non-runtime; never invoked on its own.4---56# The `.odd/` memory contract78The ODD loop's memory is a set of committed files in the observed9repository, under `.odd/`: git versions them, pull requests review them,10every user of the repository shares them — no side-channel storage,11nothing opaque. It has five kinds, each with the reference that owns12its specifics (its paths, its frontmatter fields, its recall matching,13its branch name, its commit subject, what its reply carries, and how a14stored one is shown):1516| Kind | Store | Reference |17| --- | --- | --- |18| Observation reports | `.odd/observe-run-reports/` | [references/observe-run-report.md](references/observe-run-report.md) |19| Instrumentation reports | `.odd/otel-instrumentation-reports/` | [references/otel-instrumentation-report.md](references/otel-instrumentation-report.md) |20| Maintainer rulings | `.odd/decisions.md` (findings) and `.odd/entry-classifications.md` (tree entries), written by `scripts/odd_ledger.py` | [references/decisions.md](references/decisions.md) |21| Benchmarks | `.odd/benchmarks/<name>/` | [references/benchmark.md](references/benchmark.md) |22| Custom stacks | `.odd/observability-stacks/<name>.md` | [references/observability-stack.md](references/observability-stack.md) |2324This file owns what they share; a reference states only its25specifics. A sixth kind is a new row in this table and a new26reference, never a sixth copy of these rules.2728## Reading a reference by section2930A reference is read like a stack reference — by the section a step31needs, never whole. To **persist** an artifact, read its naming and32format sections and its `## Rules`; to **recall** the baseline before33a run, its `## Recall`; to **show** a stored artifact at the end of a34mission, its `## Show` and nothing else; to **record** a decision, the35ledger reference's `## Recording a decision` and `## Rules`, and to36record a tree-entry classification, its37`## The entry-classification ledger`. The `## Return value` of a38report reference is what the persistence hands the caller, and what39`## Show` renders from.4041## Where the memory lives4243- In the **observed** (or investigated) repository, exactly where the44 kind's reference says — never in the oddyssey package, a home directory,45 or a temp path. Create the directory when it does not exist.46- **Committed**: the files stay tracked, never added to `.gitignore`.47- A report file is named `YYYY-MM-DD-HHmm-<run_name>.md`: the run's48 **UTC** start time to the minute, so two same-day runs never collide49 and a plain listing sorts chronologically — computed with `date -u`,50 never from the local clock (a session crossing local midnight while51 UTC has not names the wrong day) — then a short kebab-case slug52 naming the content, not the date. The kind's reference adds its own53 prefixes and variants.5455## The frontmatter and the body5657- A report opens with a YAML frontmatter so later runs can filter the58 store **without parsing prose**. Every field mirrors the run as it59 actually executed — defaults applied, not as requested. The kind's60 reference lists the fields and their meaning.61- The body is the producing agent's judgment **as-is**, whole: what62 each section carries is the agent's contract, stated once where the63 kind's reference says — in that reference, beside the format, or in64 the agent — and read at report time; the persistence adds nothing to65 it beyond the numbered headings and the few machine-read shapes a66 kind's script fixes and checks (its reference names them), and a67 summary cannot feed a later diff.6869## Append-only, with one exception7071- The report stores and the ruling ledgers are **append-only72 evidence**: one run, one file — never edit a stored report to73 "update" it, a new run writes a new file and the diff lives there; a74 decision is a row appended, never rewritten, and the latest row for a75 finding wins, as does the latest row for a tree entry. A report is76 never modified to carry a decision: the ledger is the decision's only77 home — and a ledger is written by the script its reference names,78 which checks the row before it lands, never by a file tool; the79 reference's prose steps are the fallback where the script cannot80 run.81- A **benchmark is living source**, not a run record: it is updated in82 place through reviewed diffs, and git history, not file accumulation,83 is its memory. It is never overwritten silently — an update is a diff84 the maintainer reviews like any other committed change. A **custom85 stack file** is living source the same way.86- The consumers keep the two apart: `/odd-verify` and `/odd-status`87 treat a commit that touches only the report stores or a ledger as88 memory, not code, while a commit that changes a benchmark or a custom89 stack file is a code change.9091## Recall: reading the memory9293For the two report stores and the benchmarks — a custom stack is94recalled by name, each ledger is one file; their references own that:9596- Run the recall script this skill carries,97 `python3 <this skill's directory>/scripts/odd_recall.py --repo <path>`98 with the mission's scope as flags — the kind's reference says which.99 **The invocation may travel with the mission**: a caller's preflight100 that already resolved the scope writes it into the mission block as101 `Recall: python3 <this skill's directory>/scripts/odd_recall.py --repo <path> --service <name> [--service <name> ...] --stack <stack> --depth <depth>`102 (the observation form — one `--service` per name, `--env` left for103 the agent to append once it has detected the environment; a benchmark104 is recalled by `--kind benchmark --service <name>` alone, an105 instrumentation report by `--kind instrumentation --project <scope>`),106 and the agent runs that line as is — never re-derived, never a flag107 guessed. The whole surface is `--repo`, `--kind <observation |108 instrumentation | benchmark>` (default `observation`), `--service`,109 `--stack`, `--env`, `--depth`, `--mode` (repeatable) and `--project`110 — nothing else: `--help` has nothing to add and the file has nothing111 to read. The script reads every stored frontmatter (a benchmark's manifest) in112 Python and prints the matches newest first, one line each; a113 report the frontmatter contract flags, or a benchmark whose manifest114 it cannot read, is listed and reported, never skipped silently. No115 frontmatter reaches the conversation until the baseline is chosen.116 A missing or empty store is a first run — the script says so, never117 fails. **By hand, only when the script cannot run**: for a report118 store, list it newest first (the filenames sort chronologically) and119 read **frontmatter blocks only**, never whole files; the benchmarks120 are a directory store, so list the directories and read the few121 manifest keys their reference names, never a whole manifest. Either122 way apply the kind's matching rules yourself; a missing or empty123 store is a first run then too — say so, never fail.124- The first line printed is the baseline: read it **by section, never125 whole** — the kind's reference names the sections a mission needs,126 and the command that prints them when it ships one.127 Reading beyond that set is the exception, for a stated need that the128 calling agent records. A benchmark recall has no baseline: the whole129 listing is the set the mission checks itself against, and its130 reference says what each column answers.131- Older matches are history: read them only when a trend or the132 evolution of one decision matters, and only the sections in question.133134## No secrets, no real identifiers135136Every file of the memory is made to be committed and shared. Never137write a token, credential, cookie, or connection string into one —138refer to access material by variable or secret name only. The same for139**real identifiers** that carry no access on their own: tenant,140workspace, subscription, resource-group or site names and GUIDs,141account or login names, home-directory paths — anything that identifies142a real customer, tenant or environment — and **every value persisted143under a remote stack's `stack_config`**, regions and the `local` stack144excepted: a log group or a profile name reads like a plain path and145identifies an account all the same; write the field's name in angle146brackets instead (`<log_group>`). A live CLI excerpt, a147configuration display, or a mission block's preflight handoff is the148likeliest source: every such value lands in the file as an obviously149fake placeholder (`Contoso`, a zeroed or patterned GUID,150`example-user`, `<scratchpad>`). On a host that runs the package's151lifecycle hooks, a hook flags what slipped through, after the write.152153## The work branch and the lone commit154155An observation report's `persist` (its reference's script) applies this156section; everywhere else it is applied by hand:157158- **Never commit on the default branch.** Before committing, compare159 `git branch --show-current` with the repository's default branch160 (`git symbolic-ref --short refs/remotes/origin/HEAD` stripped of its161 `origin/` prefix; if unset, `main` — or `master` when that is the162 checked-out branch). Only when on the default branch, create and163 switch to the work branch the kind's reference names (switching to it if164 it already exists), commit there, and say so in the reply. If165 switching is impossible, do not commit: state the path and leave the166 commit to the caller. On a host that runs the package's lifecycle167 hooks, a hook refuses the commit itself; this rule stays the168 enforcement everywhere else.169- **Commit the artifact's files alone**: `git add` the files the170 persistence just wrote, then `git commit` with the subject the171 kind's reference gives (`docs(odd): ...`) — never stage anything else; a172 dirty working tree stays untouched otherwise. If committing is173 impossible (not a git repository, or the caller said not to), state174 the path and leave the commit to the caller.175- Either way the reply states the stored path and the carrying commit176 (`git rev-parse --short HEAD` right after the commit), or177 `not committed` with the reason.178179## The reply and the synthesis180181- The persistence's return value carries the stored path, the182 carrying commit, and what its `## Show` needs to render — the183 **synthesis inputs**, quoted from the artifact where the artifact184 carries the value, never rephrased, the kind's reference lists them;185 or, when the kind ships a script that renders the synthesis from the186 stored file, the headline alone, so the block never travels through187 the reply — and never the artifact's body: an observation report188 runs 300 to 500 lines, the reply travels back into the caller's189 context, and the synthesis is its only reader. What the next wave190 needs is in the file, at the stored path.191- `## Show` renders from that return value, or reads a stored192 artifact the caller names from disk, by section, with its carrying193 commit from git (`git log -1 --format=%h -- <path>`) — never from the194 conversation's memory of the mission.195- Everything in the synthesis comes from the stored artifact: no196 backend query, no doc fetch, no re-derivation, no invented value; the197 carrying commit is the one value outside the file, and a value the198 artifact does not carry is absent from the synthesis too.199- One screen, hard cap: prefer dropping rows behind a200 `+N more in the report` marker over growing sections.201- The synthesis renders in the conversation's language; the stored202 artifact itself stays English.203- The synthesis never replaces the artifact: the next wave consumes204 the stored file — state its path, never re-inline the full body.