decision-journal
Keep a decision journal
Record a decision and the alternatives it rejected, anchored to evidence, so the reasoning survives the session — append-only, retractable, with show/trace/digest to read it back. Records the choice, not the diff — for what a change did, use describe-changes.
When it fires
Symptoms: we considered X and rejected it, why is this like this, what did we already rule out, I'm assuming Y without checking, that turned out to be wrong, write this down before you compact
Broadly applicable; the agent reaches for it across many kinds of work.
Install it
npx skills add crissmoldovan/agent-skills --skill decision-journalnode <skill-folder>/scripts/install-cli.mjs # its CLI, once, so hooks and your shell find itAny harness that can run a shell command, plus Node.js 22+ for the CLI it carries. Hooks capture actions automatically where the harness exposes them — Claude Code, Codex, Cursor and Gemini do; Cowork and ChatGPT Work do not, and there the agent records entries itself and the journal says so. Without a filesystem it degrades to structured blocks in the transcript. Git raises what a claim can point at but is not required: a design or ops journal anchors to tool calls, in a weaker voice.
What to run
agent-journal record --workspace api --kind decision \ --question "how do we bound the retry queue?" \ --chosen "in-process ring buffer, 256 entries" \ --rationale "backpressure is observable and the failure mode is dropping oldest, which we can measure" \ --rejected "redis list — needs a broker we do not run in this environment" \ --rejected "kafka — three days of setup for one queue"agent-journal record --workspace api --kind assumption \ --assumed "the upstream call is idempotent" \ --ifWrong "a retry double-charges the customer" \ --checked noagent-journal invalidate 7f3a --workspace api \ --reason "the 107 test failures were a wrong interpreter on PATH; the regression never existed"What it ships with
- The observation plane: what hooks capture, and what they don'treferences/adapters.md · 3,372 words
- Anchors: what a claim can point atreferences/anchors.md · 1,077 words
- Authoring floors: when the journal asks firstreferences/authoring-floors.md · 2,810 words
- Decay: checking a journal against the world it describedreferences/decay.md · 2,702 words
- Degraded modesreferences/degraded-modes.md · 771 words
- Digest and disclosure: rendering a journal for someone who cannot read all of itreferences/digest-and-disclosure.md · 3,105 words
- Entry kindsreferences/entry-kinds.md · 1,263 words
- Retention and deletion: tombstones, compaction, and what each does not doreferences/retention-and-deletion.md · 3,726 words
The skill, in full
Keep a decision journal
Every agent harness now records what happened. None of them record why.
A session leaves behind hundreds of tool calls — every file read, every command run, every edit applied. Not one of them contains the sentence that matters: we chose Postgres over SQLite because reconnect had to converge, and we ruled out the queue because it needed a broker we do not run. That sentence lives in the model's context for about an hour, and then compaction destroys it.
Six months later somebody asks why the code is shaped this way. The commit history answers what. The journal is where why survives — and, more valuably, where the alternatives you already rejected survive, so nobody spends a week rediscovering that they do not work.
Quickstart
The CLI ships in this skill: node scripts/agent-journal.mjs works wherever
agent-journal appears below. To put it on PATH for hooks and a bare shell, a person
runs node scripts/install-cli.mjs once; an agent asks them to. Record one decision:
agent-journal record \
--workspace my-project \
--kind decision \
--question "how do sessions find their journal?" \
--chosen "hash the git common directory" \
--rationale "a worktree and its main checkout must share one journal, or collision warnings never fire"
Later, someone finds that reasoning rested on a false premise. Retract it — no session required, which is the point; root causes are usually found in a bare shell:
agent-journal invalidate <entry-id> \
--workspace my-project \
--reason "the interpreter on PATH was wrong; the failures were never real"
Ask what the record does not cover, which is the question most audit trails cannot answer:
agent-journal coverage --workspace my-project
That prints sessions observed, sessions that recorded nothing, refused writes, sequence
gaps — and null, not [], for anything it never assessed. An empty array would claim
a clean bill of health it never established.
It also names what it could not read. A corrupt or unreadable journal exits non-zero and
lists the unreadable paths and malformed lines, because an all-zero report from a
destroyed record and one from a quiet week must not look the same.
When to Use
- A choice was made that a reasonable person could have made differently.
- You rejected an option for a reason that is not obvious from the code.
- Something surprised you, and the surprise changes what you would do next time.
- A decision rests on something you did not verify — say so; that is the most valuable entry in the journal, not the most embarrassing.
- Work is about to be compacted, and the reasoning is still in context.
Do not use it as a task log. Progress belongs in your tracker; this is not a Kanban column. Do not narrate what a diff already shows — a decision entry that restates the change adds noise and dilutes the entries that carry judgement. And do not record a decision you have not actually made yet: this skill documents choices, it does not help you make them.
Prerequisites
- The CLI runs.
agent-journal help, ornode scripts/agent-journal.mjs help, exits 0. Complete when: one does. If neither can, use references/degraded-modes.md and say so. - A workspace id. Everything is scoped to one. Pass
--workspaceexplicitly; the library can derive one from the git common directory, but the CLI does not guess. Complete when: you have a stable string that will be the same next session. - Knowing what the journal can prove here. In a code repository a claim can point at a commit. In a design or ops context it cannot, and the entry must say so instead of implying otherwise. See references/anchors.md. Complete when: you can name which anchor classes are available.
- Knowing whether anything is watching automatically. On a harness with hooks
wired, tool calls and session boundaries are observed independently of what you
write — a second witness to check your own account against, not a replacement for
it. Cite one with
--anchor tool_use:<observation-id>; find the id withtrace <tool-name>and thenshow. The adapters are not installed with this skill — they live in the repository, at https://github.com/crissmoldovan/agent-skills/tree/main/adapters. Hooks do not cover everything: no heartbeat, no confirmed permission events, and unattributed tool calls from inside a subagent. See references/adapters.md for the whole loop and before assuming a class of claim is covered that isn't. Complete when: you know whether this session's harness has an adapter wired, and — if it does —agent-journal showshows observation kinds withprovenance: "hook". Useshow, notcoverage:coveragecounts sessions, so a hooked session and a hookless one both reportsessions: 1and it cannot tell you a hook fired.
Procedure
1. Notice that a decision happened
This is the hard part, and it is where the skill earns its keep. The decisions worth recording rarely announce themselves. In practice they hide in three shapes:
- A choice that did not feel like one. You reached for a method because its name read like a read. You set a flag because the ticket said to. Nobody deliberated, so nobody wrote it down — and that is precisely the entry someone will want.
- A rejection. You considered something and moved on. The moving-on is the record.
- An assumption. You proceeded as though something were true without checking.
Complete when: you can state the decision as a question someone else could have answered differently.
2. Write the entry
agent-journal record --workspace <id> --kind decision \
--question "<what was being decided>" \
--chosen "<what you picked>" \
--rationale "<why>"
A good entry answers three things and resists the fourth:
| Field | What it holds | The failure mode |
|---|---|---|
question |
What was actually being decided | Restating the change instead of the choice |
chosen |
What you picked | Describing the implementation, not the decision |
rationale |
Why, in terms that would persuade a stranger | Post-hoc justification of a foregone conclusion |
rejected |
What you did not pick, and why not | Omitting it. This is the field nothing else captures |
Complete when: the entry would let a stranger reconstruct your reasoning without reading the diff.
3. Anchor the claim
An entry that cites nothing is a story. Anchors are the difference between this decision was taken and somebody says it was.
Anchors are selected, not recalled — the journal already recorded every file you read and every command you ran, so the candidate set is the work you just did. You are choosing from it, not remembering it.
Anchoring proves the decision happened. It does not prove the decision was right. That distinction is the single most important thing in this skill, and it is covered in references/anchors.md. A perfectly anchored entry resting on a false premise is worse than no entry, because it is citable.
agent-journal record --workspace <id> --kind decision \
--question "<what was being decided>" --chosen "<what you picked>" \
--anchor file:src/queue.ts:14 \
--anchor commit:9f2c1ab
--anchor <class>:<ref> is repeatable and splits on the first colon, so a ref keeps
its own — file:src/queue.ts:14 records the path and line, not a truncation of it. The
classes are commit, file, environment, visual, runtime, tool_use, message,
url and external; references/anchors.md says what each one
proves and, more usefully, what it does not.
Citing an anchor marks its class available. The entry's capability table records
file: known once a file anchor is on it, and leaves every other class unknown. That
is not optimism — the anchor is the evidence that the class is reachable here. What the
rule forbids is the reverse: assuming a class is available because it usually is.
Complete when: every factual claim in the entry points at something checkable, or is explicitly marked as resting on nothing.
4. Say when you consulted nothing
If a decision rested on your own priors and no source, record that. The schema has a
first-class way to say it — an influence of type model_knowledge — and it is
deliberately not the absence of a field, because "I consulted nothing" and "I forgot to
record my sources" must not look identical.
This feels like an admission. It is the most useful signal in the entire record: it tells a later reader which decisions were reasoned from evidence and which were reasoned from vibes, and no other tool surfaces that at all.
agent-journal record --workspace <id> --kind decision \
--question "..." --chosen "..." \
--influence model_knowledge:decisive
model_knowledge is the one influence type that needs no reference, because there is
nothing to point at — that is the claim. Every other type takes
--influence <type>:<role>[:<ref>], splitting on the first two colons so a URL keeps
its own: --influence url:contradicted:https://example.com/bench?a=1:2. The roles are
decisive, supporting, considered and contradicted — the last meaning I read
this and chose against it, which pairs with --rejected and is otherwise unrecoverable.
Complete when: an entry with no sources says so explicitly, as a value rather than an absence.
5. Retract what turns out to be wrong
Two different things can happen to a decision, and conflating them loses information:
- Superseded — something newer replaced it. The original was reasonable at the time, and everything built on it still stands.
- Invalidated — the premise was false. It was never sound, and everything resting on it is suppressed too, transitively.
invalidate covers the second case. The first has no separate command — pass
--supersedes <id> on the record that replaces it:
agent-journal record --workspace api --kind decision --id d-old \
--question "how do we bound the retry queue?" --chosen "fixed 256-entry buffer"
agent-journal record --workspace api --kind decision --id d-new \
--question "how do we bound the retry queue?" \
--chosen "backpressure signal, no fixed bound" --supersedes d-old
agent-journal show --workspace api --id d-old
d-old now reads outcome: reverted, live: false in that output; d-new is
unaffected — unlike invalidation, supersession never propagates to what was built on
the entry it replaces. --id on show narrows to one entry instead of the whole
workspace, which is the faster check once you know which id you are asking about.
agent-journal invalidate <id> --workspace <ws> --reason "<what was actually true>"
This works with no session, deliberately. The canonical case is a root cause found in a shell hours after the agent that wrote the entry has gone.
Suppression follows the links you recorded. An entry declares what it rests on with
--influence journal:<role>:<id>, and the projection layer walks those to a fixed point.
So invalidating an entry suppresses everything that cited it, and everything that cited
those:
agent-journal record --workspace api --kind decision --id d-bound \
--question "how do we bound the retry queue?" \
--chosen "in-process ring buffer, 256 entries"
agent-journal record --workspace api --kind finding --id f1 \
--claim "the 256 bound is never reached" --influence journal:decisive:d-bound
agent-journal invalidate d-bound --workspace api --reason "the bound was measured, not assumed"
agent-journal show --workspace api
f1 now reads outcome: invalidated, live: false alongside d-bound, without being
named in the retraction. An entry that rested on nothing you recorded is not reached —
the graph only knows the edges you gave it, which is the reason step 4 is worth the
keystrokes.
Complete when: the wrong entry is marked wrong, and anything that declared a dependence on it went with it.
6. Read the coverage before you trust the record
agent-journal coverage --workspace <id>
Silence in a journal is ambiguous — nothing decided, or nothing recorded? The coverage
report is what makes the difference legible: sessions that produced no entries, refused
writes, gaps in a source's sequence, and an explicit null for anything never assessed.
Complete when: you can state what the journal does not cover, not just what it does.
7. Render a digest, and trace from a symptom
agent-journal digest --workspace api --level team
A digest renders the journal as one markdown document, ordered by consequence rather
than time — invalidated first — with the coverage report folded onto the end. It
defaults to --level published, the version meant to leave the room, and it is a
snapshot stamped with the moment it was rendered, never the source of truth. See
references/digest-and-disclosure.md for what
disclosure does and does not gate: a private entry never appears in a digest, but its
invalidation of something else always does.
agent-journal trace f1 --workspace api
trace starts from whatever a support question actually hands you — a file, a ticket,
a runtime flag, or an entry id, indexed equally — and walks backwards through what an
entry rests on. Lookup is exact, never a substring. The walk does not stop at an
invalidated entry: f1 rests on d-bound, invalidated two steps back, and the walk
reaches it anyway, because that is frequently where "why is this like this" ends.
Complete when: you know what a reader at a given disclosure level will and will not see, and a symptom traces back to the decision it actually depends on, not just the entry that happens to mention it.
8. Check what has decayed
Entries do not stay true because they were true once. A cited file gets deleted, a
ticket closes, an interpreter on PATH is not the one a decision was made under.
agent-journal decay --workspace api --repo .
This reports two different things, never resolves either, and exits 0 regardless of
what it finds — a failing finding does not invalidate anything, and a drifted
finding is not a verdict. --repo defaults to nothing on purpose: without it every
codebase influence reads not-checkable rather than this command silently scanning
whatever directory you happened to be standing in.
Know the honest limit before you trust a passing codebase finding: path:symbol
checks that the symbol's name still appears in the file's text — a grep, not a parse.
It catches a deleted or renamed symbol. It does not catch one that kept its name and
changed what it does. See references/decay.md for the full
reference — every status, what decay does and does not look at, and why an
environment anchor's drift is reported as drifted rather than failing.
Complete when: you can name which of the five statuses a finding carries and what each one does and does not commit to, and you know that reading the report is not the same as acting on it.
9. Delete something that must not exist
A decision can be entirely sound and still contain a leaked credential or a named
person. invalidate is the wrong tool for that — it says the reasoning was wrong, and
this content might not be. tombstone takes no position on the reasoning at all; it
says the content itself must not exist.
agent-journal tombstone <id> --reason "<why this must not exist>" --workspace <id>
That appends an event and purges nothing. compact is the only command in this
package that deletes, and it is a dry run unless --apply is given — this is the one
command meant to run unattended, from a hook or CI, with nobody there to confirm it:
agent-journal compact --workspace <id> [--entry-ttl-days <n>] [--observation-ttl-days <n>] [--apply]
An omitted TTL flag means that axis never expires, not "expire everything," and
compact refuses outright on a journal it could not fully read rather than risk
purging on an incomplete view. Neither command reaches into a replica that has not
seen the tombstone yet — that replica keeps the bytes until it does; §13.2 states this
as the honest price of being able to erase anything at all. See
references/retention-and-deletion.md for the
full mechanics, including what an anchor citing purged content renders as afterward.
Complete when: you can say, for content that must go, whether invalidate or
tombstone is the right tool — and, if it is tombstone, that you ran compact
without --apply first and read what it would do before adding the flag.
10. Let a floor catch what self-triggering misses
Step 1's "notice" is voluntary; §11.3's finding is that what an agent notices on its own is roughly the complement of what causes incidents. Two authoring floors narrow that by prompting, never writing: one on a consequence-bearing observation (a mutating command, an unfamiliar host, a matching constraint), one before compaction.
agent-journal floor --kind consequence --workspace <id> [--since <ts>] [--subject <s>]
agent-journal floor --kind compaction --workspace <id> [--since <ts>]
Opt-in per workspace (AGENT_JOURNAL_FLOORS=1) — an unasked floor gets the adapter
uninstalled. See references/authoring-floors.md for
what the classifier misses, why Floor 1 can't force a flush, and Floor 2's blind spot.
Complete when: you know whether floors are on here, and treat one as a question, never an entry to accept as written.
Usage Examples
Recording a rejection, which is the entry nothing else captures:
agent-journal record --workspace api --kind decision \
--question "how do we bound the retry queue?" \
--chosen "in-process ring buffer, 256 entries" \
--rationale "backpressure is observable and the failure mode is dropping oldest, which we can measure" \
--rejected "redis list — needs a broker we do not run in this environment" \
--rejected "kafka — three days of setup for one queue"
Recording an assumption you did not verify:
agent-journal record --workspace api --kind assumption \
--assumed "the upstream call is idempotent" \
--ifWrong "a retry double-charges the customer" \
--checked no
That entry is what turns a future incident from nobody knows why into here is exactly what we assumed and never confirmed.
A human retracting an agent's finding, from a bare shell:
agent-journal invalidate 7f3a --workspace api \
--reason "the 107 test failures were a wrong interpreter on PATH; the regression never existed"
Pitfalls
- Recording what the diff already says. "Changed the timeout to 30s" is not a decision; "chose 30s because the p99 upstream is 22s and we would rather fail than queue" is.
- Treating an anchor as proof of correctness. It proves the decision happened. A well-anchored entry built on a false premise is the most dangerous artefact this system can produce, because it reads as verified.
- Writing entries only when things go well. A journal of successes is a marketing document. The rejections and the assumptions are what make it worth keeping.
- Letting the entry be a performance. If entries are read in review, the temptation is to attach a source you skimmed rather than admit you consulted nothing. That converts the most valuable field in the record into decoration.
- Reading
[]as "none found". Where the journal never assessed something it saysnull. An empty array means assessed and empty. Conflating them turns "we did not look" into "we looked and it was clean". - Assuming order implies causality. Entries from different sources are ordered for display, not for meaning. If one thing caused another, the edge between them says so — the sequence in a list does not.
- Reading a
passingcodebasefinding as "the reasoning still holds". It means the symbol's name is still in the file.decay'spath:symbolcheck is a grep, not a parse — it says nothing about whether the symbol still does what the entry said it did. See references/decay.md.
Verification
Before treating a journal as a record you can rely on:
- Every entry cites something, or admits it does not. Read the entries and check.
coveragecannot answer this for you — it reportsdowngradedAnchors: null, meaning not assessed, because it runs no retention pass. An entry that cites nothing and does not say so is a story. - The coverage report distinguishes
nullfrom[]. If a field reads as an empty array when nothing assessed it, the report is claiming a clean bill of health it never earned. That is a bug, not a clean journal. - A refused write left a trace. A recognised secret is masked and the entry is written; a payload the redactor cannot scan at all — oversized, or too deeply nested — is refused outright, and that refusal is recorded as a void. If refusals vanish silently, the gap they leave is invisible and the coverage report is lying by omission.
- The journal could be read at all.
coverageexits non-zero and listsunreadablepaths andmalformedlines when the record is damaged. Zeroes from a damaged journal mean "we could not look", not "nothing happened" — treat its counts as a floor. - Retractions took effect.
agent-journal show --workspace <ws>reportsoutcomeandliveper entry. An invalidated entry and everything that declared a dependence on it both readlive: false. - You can tell a retraction from a decision.
invalidateappends its retraction as adecisionrecord, so a workspace with two entries and one retraction shows three rows. The retraction is the one whoseretractsfield is populated; the entries it acted on carryretracts: null. A count of live decisions that forgets this is wrong by one per retraction, forever. nullis not[]inshoweither.anchors,influencesandretractsreadnullwhen the entry has none — not recorded, rather than assessed and empty.- A
decayreport is something to read, not something that gates. It exits0whether every finding ispassingor half of them arefailing— treating a clean exit code as "nothing decayed" skips the one field,findings, that actually says so.
Deeper reading
- references/anchors.md — what a claim can point at, what each anchor class actually proves, and why anchoring is one-directional.
- references/decay.md — the five statuses
decaycan report, what it does and does not check, thecodebasegrep's honest limit, and why environment drift is reported rather than resolved. - references/entry-kinds.md — decision, finding, assumption, blocker, progress, constraint: which to use and how they differ.
- references/digest-and-disclosure.md — the
three disclosure classes, why the write default and the parse default differ, how a
digest orders and gates entries, and what
tracecan and cannot find. - references/retention-and-deletion.md —
tombstones versus
invalidate, why a tombstone forfeits pure CRDT convergence,compact's dry-run default and its refusal on a damaged journal, entry TTL, and the pinning interaction between the two. - references/degraded-modes.md — running without the CLI, without a filesystem, or without hooks, and how to say so honestly.
- references/adapters.md — the observation plane hooks feed
automatically: what it proves that self-reported entries can't, obtaining and
installing the Claude Code and Codex adapters (they ship in the repository, not with
this skill), verifying arrival with
coverage, the capture → find → cite loop for anchoring an entry to an observation, what still has to be hand-anchored, and the honest, unequal status of each adapter. - references/authoring-floors.md — the two floors that prompt regardless of judgement: what fires each, opting in, what the classifier misses, and why Floor 1 doesn't force a pre-compaction flush and Floor 2 misses a failed tool call.