decision-journal / references/decay.md
Decay: checking a journal against the world it described
A reference shipped with the skill · 2,702 words
Decay: checking a journal against the world it described
An entry does not stay true because it was true once. The file it cites gets deleted.
The ticket it cites gets closed. The interpreter it ran under is not the one on PATH
today. decay is the command that asks, of everything already in the journal, does
the world still look like this said it did — and reports what it finds without acting
on any of it.
references/anchors.md introduces the distinction between rot (did a
source move or die) and premise (was the reasoning ever sound) in brief. This
document is the full reference: every status decay can report, exactly what it does
and does not look at, and the two limits worth knowing before you trust its output.
Running it
agent-journal decay --workspace <id> [--repo <path>]
--repo is the only thing beyond --workspace, and it is optional — see "The
codebase ref convention" below for what omitting it means. Like every other command
here, an unrecognised flag is refused rather than silently ignored.
decay exits 0 whenever the journal itself could read cleanly, regardless of what
the findings say — a report full of failing entries exits the same as a report
full of passing ones. That is not an oversight; see "Nothing is auto-resolved" below.
Exit 1 means the journal was damaged (unreadable segments, malformed lines), the same
floor-not-total warning coverage, show and trace already give — and is unrelated
to any individual finding's status.
Five statuses, and what each one commits to
| Status | Says | Never means |
|---|---|---|
passing |
Checked. The source still holds. | — |
failing |
Checked. The source no longer holds, or the citation was broken to begin with. | "It probably still holds" |
drifted |
Checked. It differs — but the difference is a question for a human, not a verdict. | "It is wrong" |
not-checkable |
Nothing could ever check this, or the tools to check it were not supplied for this run. | "It is fine" |
not-implemented |
A checker could exist for this. This release does not ship one. | "It is fine", or "it can never be checked" |
The two that get confused are not-checkable and passing. They must never collapse
into each other — a person influence that comes back anything other than
not-checkable is a bug, because nothing in this tool can re-interview a person. Read
not-checkable as silence, not as clean.
The other confusable pair is drifted and failing, and the difference is deliberate
rather than cosmetic — see "Environment drift" below for why it exists as a fifth
status instead of folding into failing.
What decay actually looks at
Two things, and only two:
- Every live entry's
influences— the nine types in references/anchors.md:journal,tool_result,codebase,url,ticket,document,person,model_knowledge,conversation. environment-class anchors specifically.
Every other anchor class is not decay-checked in this release. commit, file,
visual, runtime, tool_use, message, url and external anchors produce no
finding at all — not not-checkable, not anything. Citing one is still valuable (it is
what references/anchors.md is about), but decay has nothing to say
about it. Do not read a clean decay report as "every anchor on this entry was
verified"; read it as "every influence, and any environment anchor, was verified."
What journal and tool_result each verify
Both resolve their ref inside this journal, and both check the kind of what they
find, because a citation that points the wrong way is a defect in the entry rather
than decay in a source:
journal— the ref must name an entry, and that entry must still be live. It reportsfailingif the entry is absent, superseded, invalidated, avoid, or an observation (cite an observation astool_resultinstead).tool_result— the ref must name an observation, and this is a presence check only. It confirms the observation is still in the journal. It does not confirm that what the tool reported then is still true now. Atool_resultreadingpassingmeans "that row is still there", never "that command would still print this". Nothing in this release re-runs anything.
Citing a void fails on both. A void is the journal's own record that a write was
refused — reading it as a surviving source would say "your evidence still holds"
about a row whose entire content is that nothing was written.
An entry that was itself directly superseded or invalidated is skipped — its own
sources stopped mattering the moment it stopped counting. An entry that merely cites
a casualty through a journal influence is not skipped: that citation is exactly what
reports failing, which is how a reader learns the entry's reasoning now rests on
something withdrawn.
The codebase ref convention, and its honest limit
A codebase influence's ref is either a bare path or path:symbolName:
agent-journal record --workspace decay-demo --kind decision --id d-decay-check \
--question "how does decay check a codebase influence?" \
--chosen "existence, plus a text-contains scan for a named symbol" \
--influence codebase:decisive:packages/agent-journal/src/decay.ts \
--influence codebase:supporting:packages/agent-journal/src/decay.ts:computeDecay
packages/agent-journal/src/decay.ts alone checks that the file exists.
packages/agent-journal/src/decay.ts:computeDecay additionally requires the text
computeDecay to appear somewhere in that file.
That second check is a grep, not a parse. It catches a deleted file. It catches a
renamed or removed symbol — computeDecay gone from the text reports failing. It
does not catch a symbol that kept its name and changed what it does. If
computeDecay's signature or behaviour changed while the identifier stayed exactly as
it is, this check has no way to know, and the finding would still read passing. A
passing codebase finding means the name is still there, never the code still
means what the entry said it meant. Overclaiming here — parsing every language a
journal might cite, understanding renames, tracking behaviour — is not a thing this
release attempts, and the reference says so rather than implying otherwise.
--repo is what makes any of this checkable at all, and it defaults to nothing:
agent-journal decay --workspace decay-demo
{
"findings": [
{ "entryId": "d-decay-check", "type": "codebase",
"ref": "packages/agent-journal/src/decay.ts",
"status": "not-checkable", "detail": "no codebase index was provided for this run" },
{ "entryId": "d-decay-check", "type": "codebase",
"ref": "packages/agent-journal/src/decay.ts:computeDecay",
"status": "not-checkable", "detail": "no codebase index was provided for this run" }
],
"checked": 0, "notCheckable": 2, "notImplemented": 0, "drifted": 0
}
That is deliberate, not a missing default. Silently scanning whatever directory the
caller happens to be standing in is a surprise; not-checkable is the honest status
for "nobody told this run where to look." Point it at a checkout and the same refs
resolve — run from the root of this repository, --repo . names it:
agent-journal decay --workspace decay-demo --repo .
{
"findings": [
{ "entryId": "d-decay-check", "type": "codebase",
"ref": "packages/agent-journal/src/decay.ts",
"status": "passing", "detail": "\"packages/agent-journal/src/decay.ts\" is present in the codebase" },
{ "entryId": "d-decay-check", "type": "codebase",
"ref": "packages/agent-journal/src/decay.ts:computeDecay",
"status": "passing", "detail": "\"packages/agent-journal/src/decay.ts:computeDecay\" is present in the codebase" }
],
"checked": 2, "notCheckable": 0, "notImplemented": 0, "drifted": 0
}
--repo is not remembered between runs — omit it on the next invocation and these two
findings go back to not-checkable, not to whatever they were last time. A ref that
resolves outside --repo (by .., a symlink, or an absolute path pointing elsewhere)
is refused the same way: omitted from what gets checked, reported not-checkable
rather than guessed at either direction.
Environment drift: why this check exists at all
This is not a feature that happened to get added. It is the reason the plan exists.
The design's canonical failure looks like this: a session runs against the wrong
interpreter on PATH. Tests fail for environmental reasons. An agent investigates them
as a genuine regression and writes an entry that is perfectly formed — real anchors,
a real failing suite, an honest note that nothing was consulted beyond the failure
itself, high stated confidence. Every anchoring rule this skill teaches is satisfied.
The conclusion is worthless. Anchoring proves a decision happened; it says nothing
about whether the premise it rested on was true, and a well-anchored entry built on a
false premise is more dangerous than an unanchored one, because it reads as verified.
An environment anchor is what makes that premise checkable after the fact. Capture
one, and a decision can point at the toolchain it actually ran under:
agent-journal observe --workspace decay-demo --kind environment --id obs-node-pin \
--interpreter /opt/build/bin/node --version v18.20.4
observed obs-node-pin
observe takes --id the same way record does — passed here so this example needs
no id copied out of the previous command's output. Omit it in practice and observe
generates one, printed the same way.
agent-journal record --workspace decay-demo --kind decision --id d-node-pin \
--question "which interpreter must the suite run under?" \
--chosen "the version pinned in package.json engines" \
--anchor environment:obs-node-pin
decay compares that observation's interpreter and version against the machine
decay itself is running on right now:
agent-journal decay --workspace decay-demo
{ "entryId": "d-node-pin", "type": "environment",
"ref": "obs-node-pin",
"status": "drifted",
"detail": "environment drifted: recorded /opt/build/bin/node@v18.20.4, now /[REDACTED]/.local/share/node/bin/node@v26.7.0" }
(The live interpreter path is redacted before it ever reaches stdout — the same
machine-identifying-path rule spec §4.3 requires for a stored environment
observation applies here to a freshly captured one, since this is the one place a raw
process.execPath could otherwise leak straight into a report.)
This is drifted, deliberately never failing. A moved source is broken — a
deleted file, a closed ticket, a superseded entry. A moved environment may simply be
a different machine: a laptop instead of CI, a container instead of bare metal, a
newer patch release nobody needs to worry about. Collapsing the two would train people
to ignore the flag exactly the way §10.1 already warns against for a living source
flagged on every save — except here what would get trained away is the one check this
whole plan was built to have. drifted is surfaced for a human to judge, never
auto-invalidated: it says the ground under this entry has moved, go look, not this
entry is wrong.
A structurally broken anchor — no observation matching the ref, because it was never
recorded or was mistyped — is failing, not drifted. That is a dead citation, the
same class of problem a broken journal or tool_result reference is, and belongs
with those, not with a legitimate machine difference.
The comparison is narrower than the whole Environment shape. Only interpreter
and version are compared. platform, packageManager and flags can all differ
between the recorded observation and the machine running decay without ever
producing a drifted finding — those fields are captured and stored, but this check
does not read them. A container field or a CI-runner label changing is invisible to
this specific check.
Nothing is auto-resolved
A failing finding does not invalidate the entry that cited the broken source. A
drifted finding does not mark anything wrong. Spec §10.1, verbatim: a decision does
not become wrong because a source moved. decay reports; a human, or
agent-journal invalidate, acts.
One report shows the whole shape at once — a broken citation, a deferred type on every row, a genuine drift, and two passing codebase refs, side by side, one exit code:
agent-journal record --workspace decay-demo --kind finding --id f-cites-ghost \
--claim "the interpreter pin still matches production" \
--influence journal:decisive:d-does-not-exist
agent-journal decay --workspace decay-demo --repo .
{
"findings": [
{ "entryId": "d-decay-check", "type": "codebase", "status": "passing", "...": "…" },
{ "entryId": "d-decay-check", "type": "codebase", "status": "passing", "...": "…" },
{ "entryId": "d-node-pin", "type": "environment", "status": "drifted", "...": "…" },
{ "entryId": "f-cites-ghost", "type": "journal", "ref": "d-does-not-exist",
"status": "failing", "detail": "cited entry \"d-does-not-exist\" is not present in this journal" }
],
"checked": 3, "notCheckable": 0, "notImplemented": 0, "drifted": 1
}
Exit code 0. Nothing here blocked the write above it, and nothing here will block
the next one — decay is a report to read, not a gate.
(record gives one warning earlier and independently of decay: citing a journal
influence or a retraction edge whose id is not yet in the workspace prints
WARNING: no entry with id … is present in this workspace to stderr at write time,
exit 0. It is not refused, because across replicas the target can legitimately arrive
later. decay, run afterward, is what catches the same break once enough time has
passed that "not yet synced" is no longer the likely explanation.)
Deferred, and why — not a property of the source
checked (passing + failing) plus notCheckable plus notImplemented plus
drifted account for every finding; each finding lands in exactly one of the four
counters, never zero and never two:
agent-journal record --workspace decay-demo --kind decision --id d-deferred-sources \
--question "which sources back this decision?" \
--chosen "a mix, and the record says which kind each one is" \
--influence url:supporting:https://example.com/bench \
--influence ticket:supporting:JIRA-1102 \
--influence document:supporting:design-doc-v3 \
--influence person:considered:reviewer \
--influence model_knowledge:decisive \
--influence conversation:considered:standup-2026-09-01
agent-journal decay --workspace decay-demo
| Influence type | Status | Why |
|---|---|---|
url |
not-implemented |
Checking it means fetching it — network egress this release does not perform. |
ticket |
not-implemented |
Checking it means calling a tracker's API — a credential this release does not hold. |
document |
not-implemented |
There is no ref convention yet for what a "document" points at or how to resolve it. |
person |
not-checkable |
Nothing can re-interview a person. This is permanent, not a future release. |
model_knowledge |
not-checkable |
There is nothing to point at — that is the claim it makes. Permanent. |
premise[] is deferred too
§10.1 conditions the premise re-check on a finding declaring premise[] and an
environment anchor. Only the environment half ships. A finding whose premises are now
plainly false, but whose interpreter has not moved, reports clean — because nothing
reads premise[] at all.
This is named here rather than left silent for the same reason visual and living
are: a gap a reader can see is a gap they can work around. Re-checking a premise means
re-running the reasoning that produced it, and this release does not attempt that.
Environment drift is the one premise check that can be made deterministic, which is why
it is the one that shipped.
| conversation | not-checkable | This journal does not keep transcripts to re-check against. Permanent. |
The line that matters: url, ticket and document are not-implemented — a
property of this release, and a future one could add a checker for any of them.
person, model_knowledge and conversation are not-checkable — a property of
the source itself, and no future checker changes that. Confusing "we haven't built
it yet" with "it cannot be built" understates the first and overstates the second.
Two more things named in the spec are deferred for reasons worth stating rather than leaving as a silent gap:
visualanchors are not decay-checked at all, for the same reason listed under "What decay actually looks at" above — nothing in this release even attempts it — and separately, nothing in this release writes one either. §10.1 describes comparing a design file's version id at decision-relevant granularity; no adapter or command here produces that observation yet, so there is nothing to compare even in principle.livingdoes not exist as a flag in this release. Spec §10.1 describes marking a sourcelivingto exempt it from content-hash rot — a design file changes on every save, and flagging that daily trains people to ignore rot flags entirely. But no content-hash rot check ships here at all;codebase's check is existence and a text scan, never a hash. Alivingflag would have nothing to suppress. Adding one now would be a flag whose only stored effect is to be stored — worse than not having it, because it would look like it did something.