# Plan status must be derived from evidence, not declared by hand

audience: AI coding agents first.

status: IDLE
task IDs: —
source request: owner, 2026-08-15, reconciling the plan index against actual git
history after a prior session died mid-fire: the bookkeeping in `docs/plans/INDEX.md`
had drifted so far from reality that plans looking abandoned for days turned out to be
finished and never marked, while the index still counted them ACTIVE.

## Outcome

A plan's status is DERIVED from evidence — landed commits, live branches, a running
worker — never declared by hand and left to rot. The index stops being fiction.

## The finding

The previous session died in a fire (crash, auth expiry, or a closed terminal — the
exact cause is not recoverable, only the effect). The work got done. The bookkeeping did
not. Plans that looked "abandoned since 2026-08-13" turned out to be **finished and
never marked**.

Measured 2026-08-15:

- [K3s migration and node enrollment](2026-08-10-k3s-migration-execution.md): 159
  commits, 35 branches, including a commit titled "Record successful k3s Factory
  acceptance" (`c5fb2b8bd`) and a dedicated `refs/heads/receipt/factory-k3s-delivery`
  ref. Index said ACTIVE; three of eight phases were landed and deployed.
- ask-gpt (bounded prompt submission + isolated X display startup): 61 commits between
  them; both plan files already said `Status: DONE` internally, with landed shas that
  verify as ancestors of `origin/main`. The index still said ACTIVE for both.
- ask-gpt trusted attachments: 28 commits plus live worktrees — genuinely unfinished
  (file says `Status: ACTIVE`), unlike its two DONE siblings above. Not every plan in a
  cluster shares a fate; each one needs its own check.
- Of four plans with **no artifacts at all** under any recovery surface (worktrees,
  branches, rescue refs, vault, stashes), only QuietContext was genuinely never started.
  The other three had a status label that simply hadn't been checked against anything.

The index showed 31 ACTIVE / 11 IDLE at session start. That count was fiction: several
of the ACTIVE rows were plans nobody was executing, and at least three were plans that
had already shipped.

## Why it matters

Status is written once, by hand, by an agent that may then die — crash, auth expiry, a
closed terminal, an OOM kill. Nothing after that point reconciles the written status
against what git actually contains. Left alone, the registry decays from a record of
intent into a record of nothing in particular.

That decay is the direct, measured cause of the owner's single most expensive recurring
pain: being asked to re-approve or re-describe work that was already built, because the
index told an agent it was still open.

## Signals that already exist and cost nothing to check

Before trusting a plan's own status line or an index row, check:

- every commit sha the plan cites: `git merge-base --is-ancestor <sha> origin/main`
  (exit 0 = landed).
- whether any branch or worktree tied to the plan is ahead of `origin/main` (live,
  unlanded work still exists there).
- the last commit date on the plan file itself (a proxy for last real attention, not
  proof of anything by itself).
- whether a named worker/process claimed in the receipt is actually alive.

## Decision rule

A plan whose cited commits are ALL landed and which has NO live branch or worktree
ahead of `origin/main` is DONE — whatever the file's own status line says. Flip the
file's status line to match the evidence, not the other way around; a plan file's
prose status is a claim, not a fact, until it is checked.

## Relationship to the requests board

Same defect, same cure as request-intake dedup
([2026-08-14-request-intake.md](2026-08-14-request-intake.md)): the board has to
distinguish "in progress" from "finished" from "abandoned mid-flight in a terminal that
closed" — without a human, or a since-dead agent, having written the true state down.
The request board's dedup problem and the plan index's status-drift problem are the
same underlying gap: written intent that nothing keeps honest against reality.

## Method warning (recorded because it nearly produced a false report)

A content search for `k3s`/`ask-gpt` across the repo returned zero matches — purely
because the search was passed through a command wrapper as an interpolated shell
string, and quoting broke silently. That zero-match result almost got reported as
"these plans were never started." Two fixes, both mandatory when auditing plan status
this way:

1. Pass git arguments as an argv list, never as an interpolated shell string, when a
   wrapper or subprocess call is involved.
2. Treat any zero-result search as suspicious before believing it — sanity-check
   against at least one path or sha you already know exists.

A second, unrelated instance of the same evidence-before-belief lesson showed up while
executing this documentation task: a worktree (`plan-reconcile-record`) reported a
plan file as missing (`docs/plans/2026-08-15-laptop-build-containment.md`), when the
file was actually present, committed, and complete — the worktree's git index had
partially failed to populate under machine load, not the file. Don't trust an absence
signal (missing file, zero search hits, "ACTIVE" label) without checking it against a
second, independent source before writing it down as fact.

## Verification scope of this pass

Every sha this reconciliation cited as a reason to flip a status was checked with
`git merge-base --is-ancestor <sha> origin/main` before the status was changed:
`c74ffba55` (lsp reaper), `c5fb2b8bd` (k3s Factory acceptance commit), and `08a88c193`
(requests board) all verified as ancestors of `origin/main`. The commit/branch/count
figures quoted elsewhere in this reconciliation pass (159 commits, 61 commits, 45 idle
servers reaped, load 75 -> ~25, 180/200 offload runs) are transcribed from the source
session's own measurements and scratch files, not independently re-measured here.

## Raw evidence locations

- `/home/user/.cache/agent-tmp/claude-1000/-home-user-Projects-overdeck/f7206ccd-6b70-4951-aca7-740f2f4cab41/scratchpad/plan-audit.json`
  — 49 open plans with landed/unlanded sha counts, live branches, last-touched dates.
- `/home/user/.cache/agent-tmp/claude-1000/-home-user-Projects-overdeck/f7206ccd-6b70-4951-aca7-740f2f4cab41/scratchpad/lost-search.json`
  — per-plan artifact counts across 1,130 rescue refs, 27 vault entries, 9 stashes, 6
  `wip/*` branches, 577 branches, 21 worktrees.

These are session-scratch files, not durable artifacts — the counts above are copied
into this plan and into [2026-08-11-wip-worktree-triage.md](2026-08-11-wip-worktree-triage.md)
so the finding survives even if the scratch directory is cleared.

## Constraints

- Never flip a status on vibes; every change needs a cited command and its result.
- Never delete a plan file as part of reconciliation; SUPERSEDED/REJECTED/DONE all
  preserve history.
- Reconciliation is read-only against git — it must never rewrite commits, branches, or
  refs to "clean up" evidence.

## Terminal slice — the index status column retires (registered 2026-08-15)

The end state this plan exists for, previously only an implied consequence and therefore
exactly the shape of work that falls between lanes: once the requests board carries
everything the index carries and status is DERIVED from evidence, the board's store
becomes MORE truthful than the hand-written index, and the index's status column stops
being maintained at all.

Dependency chain, in order (owner-confirmed 2026-08-15):
1. Board slices — worker association, dedup surfacing, honest counts, multi-project
   enrolment ([2026-08-14-request-intake.md](2026-08-14-request-intake.md)).
2. Agent-judgement writer — without it, work that never passes through a skill invocation
   still needs the markdown index as its intake.
3. Derived status (this plan) — commits-on-main, live branches, pid-verified workers.
4. THEN the retirement itself: the standing session-start instruction ("read the index")
   repoints at the board's store, and the status column is dropped from INDEX.md.

Acceptance, one line: an agent at session start reads current work from the store, and
`docs/plans/INDEX.md` no longer contains a status column anyone maintains by hand.
Boundary that stays: specs and plan DOCUMENTS remain markdown files per the owner's
ruling — only the STATUS bookkeeping moves; the registry of meaning does not.

## Next executable action

None currently claimed. Candidate next step, when picked up: build the reconciliation
check above into a script (`git merge-base --is-ancestor` sweep over every plan's cited
shas + branch-ahead check) that can run periodically against `docs/plans/INDEX.md` and
flag rows where the written status disagrees with the evidence, instead of relying on a
human or an agent session to catch drift by hand.
