dira decision memory for AI coding agents
$dira whydec-0024
int-0002Zero-ceremony operation — one binary, no server, no daemonactive 2026-07-29
dec-0003No model client in the binary — semantic extraction is delegated to the live sessionaccepted 2026-07-29
dec-0021The regex tier stages decisions only, and `alternatives` is required except when stagedaccepted 2026-07-31
dec-0022Confirming a regex-staged decision promotes it to the semantic tier; it does not accept itaccepted 2026-07-31
dec-0024`n` disposes of the capture, not the option; a decision against something is still a `y`accepted 2026-07-31
✗ Let `n` record a `rejected` decision - the option was weighed and refusedThis is the reading that has to be refused explicitly, because it is the one the mockup's word "Reject" suggests.
✗ Add a field or a state so a rejected capture can stay in the ledger, marked`entry.schema.json` is `additionalProperties: false`, so this is a schema change, and dec-0019 already worked out the field-level test: every added field must be understandable at every tier.
✗ Write a `note` tombstone so the rejected capture survives as exhaustThree separate objections and any one is enough.
✗ Make `n` a skip - the entry stays `staged` and the queue moves onThe queue then never empties, and `staged` acquires a third meaning on top of the two dec-0022 already gave it - awaiting a human, awaiting extraction, and now refused-but-still-here.
…1 further refusals on record
int-0002Zero-ceremony operation — one binary, no server, no daemon
dec-0003No model client in the binary — semantic extraction is delegated to the live session
dec-0021The regex tier stages decisions only, and `alternatives` is required except when staged
dec-0022Confirming a regex-staged decision promotes it to the semantic tier; it does not accept it
dec-0024`n` disposes of the capture, not the option; a decision against something is still a `y`
✗Let `n` record a `rejected` decision - the option was weighed and refused
✗Add a field or a state so a rejected capture can stay in the ledger, marked
✗Write a `note` tombstone so the rejected capture survives as exhaust
✗Make `n` a skip - the entry stays `staged` and the queue moves on
…1 further refusals on record
dec-0024 ◆ accepted 2026-07-31

Arising from dec-0003 — No model client in the binary — semantic extraction is delegated to the live session

`n` disposes of the capture, not the option; a decision against something is still a `y`

Pressing `n` on a staged entry says **the regex misread me, this was never a decision**. It disposes of the capture, and the entry is deleted.

It does not mean *we considered that and decided against it*. That sentence describes a real decision, and a real decision goes to `y`.

The two meanings, and why only one of them is a keystroke

The mockup has one word, "Reject", standing in front of two unrelated acts:

1. **The capture was wrong.** `dira sniff` matched decision *shape* — an imperative followed by a contrast (`dec-0021`) — in a sentence that announced nothing. There is no decision here and never was. Roughly one staged entry in four is this (`docs/decisions-pending/E2-L1-report.md` §3.3). 2. **The option was refused.** Something genuine was weighed and turned down.

Only (1) is a statement about the *record*. (2) is a statement about the *world*, and the ledger already has a place for it — a `rejected` decision, which `entry.schema.json` calls "first-class and permanent: the substrate `dira check` reads to stop relitigation."

The thing that separates them is not taste, it is what each one can be written from. (1) needs nothing the human did not already supply by pressing a key. (2) needs the reasoning, and the human pressing `n` at 2am supplied none.

What the schema forces, quoted

`entry.schema.json`, in the decision branch, on any state other than `staged`:

> `"required": ["alternatives"]`, `"properties": { "alternatives": { "minItems": 1 } }` — > *"Once a decision leaves `staged`, a human has stood behind it and the roads not > taken are part of what they stood behind. `alternatives: []` satisfied the letter > of the old rule while asserting that there were none — which is exactly the > assertion this field exists to prevent."*

`rejected` is one of those states. So the "record a rejected decision" reading is not merely undesirable — from a bare keystroke it is **unwritable**, because the regex tier writes zero alternatives and a keystroke supplies none. The only moves available are to invent a `why_not` (forbidden by `dec-0003`, named as invention by `dec-0019`) or to write `alternatives: []` (refused by `dec-0021`, and by the Go validator before it).

**Where the ≥1 alternative comes from, for the reading that does store a `rejected` decision.** From the semantic tier, via `y`. `dec-0022` settled that confirming a regex-staged entry hands it to extraction rather than accepting it; that mechanism is verdict-agnostic, and a decision that landed on "we are not doing this" needs exactly the same extraction as one that landed on "we are". So meaning (2) already has a keystroke, and it is not `n`. This decision adds no mechanism to serve it — it points at the one that exists.

What deletion makes unrecoverable, and what mitigates it

Deleted: the entry's `title` and its `source.excerpt` — dira's copy of the transcript fragment the match was made on — plus the `created` stamp and the session id. `dira sniff` runs from the `Stop` hook, so a staged file can be minutes old and was very likely never committed; git will not have it.

Four things bound that loss, and the fourth is the honest one.

1. **Single-level undo.** `u` restores the last disposition, byte-identical, compared by sha256 (E2-L4-T5). The keystroke that is destructive is the one the design already required an undo for. 2. **The excerpt is a quotation, not the original.** The transcript it was cut from is still on disk and still in the session that produced it. What is lost is dira's pointer to the evidence, not the evidence. 3. **Nothing citable is destroyed.** A staged decision is never enforcement substrate — `dira check` cannot read one, `dira why` will not chain through one, and no brief renders it. Deleting one removes a proposal, not a record. 4. **The reject signal is lost, and no undo brings it back.** Out-of-sample precision was measured at ≈73% by hand over four transcripts. A ledger that kept its rejects would be a standing true-negative corpus for tuning `internal/sniff`; deletion means every future measurement is another manual pass. This is a real cost of the reading and it is accepted, not solved — the second alternative above carries the `revisit_if` that would reopen it.

The one thing this reading breaks, stated rather than discovered later

`internal/sniff` dedupes candidates by title **against the ledger** (`existingTitles` in `stage.go`): a sentence is staged once because a staged entry with that title is already there. Deleting the entry removes the only record that the sentence was ever seen, and the `Stop` hook fires against a transcript that only grows — so **the next `Stop` in the same session re-stages the sentence the human just rejected**.

That is not hypothetical and it is not this lane's file to fix; `docs/plan/lanes/E2.md` scopes E2-L4 to the disposition flow and the dedupe lives in the capture tier. It is recorded here, on the decision that causes it, so that whoever owns the fix finds the reasoning rather than the symptom. The shape of the fix is a session-scoped memory of what was rejected in *this* run — which is process state, not ledger state, and therefore does not reopen any of the alternatives above.

What this costs the surface copy

Nothing in `docs/design/screens/s3-distill.html` has to change, but the register does: the key is "not a decision", not "reject". `Reject` names the option; `n` disposes of the capture. Getting that word right on the card is the cheapest place to stop the two meanings collapsing back together.

Alternatives on record — 5

✗Let `n` record a `rejected` decision - the option was weighed and refusedrefused This is the reading that has to be refused explicitly, because it is the one the mockup's word "Reject" suggests.

It fails twice. It is unwritable: the schema requires `minItems: 1` on `alternatives` the moment a decision leaves `staged`, `dira sniff` writes none, and the only way a keystroke could satisfy it is by inventing a why_not - which dec-0003 forbids the regex tier and dec-0019 names as invention. And it is dishonest even where it is writable: a `rejected` decision is enforcement substrate, so `dira check` would quote a regex misfire back at the user three weeks later as their own recorded reasoning. Losing a capture is recoverable; being told you decided something you never decided is not.

✗Add a field or a state so a rejected capture can stay in the ledger, markedrefused `entry.schema.json` is `additionalProperties: false`, so this is a schema change, and dec-0019 already worked out the field-level test: every added field must be understandable at every tier.

A `rejected_capture` mark is understood by exactly one surface - this queue - and by no reader, no brief, no `dira check` and no ledger that inherits from this one. It also buys nothing the deletion does not: the entry it preserves is a title and an excerpt nobody will ever read, kept in the one place that is supposed to hold only things somebody stood behind.

revisit ifa measurement of `dira sniff` needs a standing corpus of true negatives that the transcripts themselves cannot supply

✗Write a `note` tombstone so the rejected capture survives as exhaustrefused Three separate objections and any one is enough.

It makes the reject path two ledger mutations instead of one, on the path taken roughly 27% of the time (E2-L1-report.md 3.3), which is the cost the whole one-keystroke design is spending its budget on. A note surfaces once in the next brief by design, so the reward for rejecting a regex misfire is being shown it again tomorrow. And cst-0002 closed the kind set on the argument that every plausible addition is a tool dira promised not to become; using `note` as the bin for capture exhaust is that drift arriving through the kind set's own escape hatch rather than past it.

✗Make `n` a skip - the entry stays `staged` and the queue moves onrefused The queue then never empties, and `staged` acquires a third meaning on top of the two dec-0022 already gave it - awaiting a human, awaiting extraction, and now refused-but-still-here.

Every subsequent run re-presents the same rejects, so the reject cost is not one byte but one byte per session forever, and the lane's own promise that a second `dira distill` exits 0 printing one line becomes unreachable.

✗Split the keystroke - `n` for "not a decision", a second key for "decided against it"refused The second key's correct behaviour turns out to be `y`'s, exactly: a decision against something is a real decision whose reasoning the semantic tier has to extract, which is what dec-0022 made `y` do.

So this adds a fifth key to a four-key muscle-memory loop to express a distinction the mechanism does not act on. If the distinction ever does change what happens, it will be because extraction wants to be told the expected verdict first - and that is a parameter on the promotion, not a new key on the queue.

revisit ifthe semantic tier needs the expected verdict as an input before it will extract a rejected decision

Every decision in this repo is recorded like this one.

Keep your own →