# Task lifecycle owned by Overdeck

status: ACTIVE
board: manual-725e183ecf8b53c8
source request: 2026-08-16 owner ruling, verbatim: "there is a big hole here, and its 'Blocked' Needing me. since now there was a blocker waiting for me for a while that had no way to be registed on the board, since we have no pipeline for this and no ticket lifecycle defined anywhere. it's just wishful thinking that agents will use the requests system - and it's not happening. overdeck needs to own not only the git lifecycle but also the task lifecycle."

## Diagnosis — why "Blocked — needs you" reads 0 forever

The lifecycle verbs do not exist. Measured 2026-08-16 against the live CLI and collector:
`od-requests add` writes `asked`; `od-requests claim` hardcodes `in_flight`; there is NO path
to `blocked` and NO path to `shipped` from any writer. Agents cannot move rows even when
willing. The empty column is structural, not (only) disciplinary. The proof case: the registry
gateway sat hours waiting on the owner's GitHub token with no way to say so on the board.

## Doctrine

Overdeck owns the task lifecycle the way it owns the git lifecycle: the paths agents already
walk mutate the board as a side effect, and guards make the wrong path fail. Never a request
for agent etiquette. Server-owned transitions; writers call verbs; exact-`work_key` dedup
(landed 2026-08-16 in the intake lane) is the convergence mechanism.

## Lifecycle (server-validated, collector-owned)

```
asked → claimed(in_flight) → blocked(needs-owner) → in_flight → shipped
          ↘ canceled          ↘ orphaned → in_flight
                                  ↘ canceled
```

Transitions carry who (worker@host, owner, or session ledger), when, and a reason for
owner-blocked, orphaned, and canceled exits. `orphaned` is authored only by the canonical
session classifier after it proves both a dead worker and uncommitted files, and only for an
already-associated `in_flight` request with the exact `session_id`; generic create/update routes
cannot fabricate it. `blocked` is exclusively “needs the OWNER” — an agent waiting on another
agent remains `in_flight`. `canceled` and `shipped` are terminal; `orphaned` is recoverable by a
new claim. Invalid transitions return 409 with the current state.

## Writers, one per seam agents already cross

| verb | seam | slice |
|---|---|---|
| claim | worktree claim / cdx dispatch (live today) | done |
| blocked | AskUserQuestion / permission-prompt hook — the literal moment an agent needs the owner — flips its row with the question text as reason | S1 |
| blocked | explicit `od-requests block <work_key> --reason "…"` for everything else | S1 |
| unblock → in_flight | next claim/dispatch touching the key; owner action in the UI | S2 |
| shipped | land-queue verdict + deploy stamp flip the row by branch/ticket key — the conductor knows, the agent does not have to | S3 (phase-2 roadmap item 1) |
| orphaned | canonical dead-and-dirty session classifier + exact `session_id` association | S4 (roadmap item 5) |
| canceled | explicit `od-requests cancel <work_key> --reason "…"` | S4 |

## Enforcement — the gate that replaces hope

S1 includes extending the say-the-word Stop gate: a closing message that tells the owner
"waiting on you / you must provide X / blocked until you…" FAILS the stop unless a `blocked`
row exists for the work key (or the message quotes the row id it just created). Same mechanism
that made worktree isolation real. Fail-open on collector outage — the gate never strands a
turn on infra.

## Slices

- S1 — `block`/`unblock` verbs (CLI + collector transition validation) + AskUserQuestion
  writer hook + Stop-gate check. Evidence: a real blocked row visible in the owner column,
  created by a hook, cleared by a claim.
- S2 — unblock path + board card actions (owner marks provided / answers inline).
  REVIEW GATE (sol 2026-08-16): an authenticated web mutation TRANSPORT exists but no
  requests UI mutation does — S2 must name the exact endpoint it adds; and the documented
  "quotes the row id" Stop-gate exception is NOT implemented — S2 either implements+tests
  it or this plan drops the claim (dropped: the mapping-file check is the contract).
- S3 — lander flips shipped (branch/ticket work_key), direct-land seam via the git shim.
- S4 — orphaned rows from the session ledger; board surfaces them distinctly.
  REVIEW GATE (sol 2026-08-16), all four must be designed before S4 launches:
  (1) adding `orphaned` requires a SQLite table REBUILD (CHECK constraint is closed; no
  in-place alter) preserving rows/indexes/FKs, plus RequestState/STATES/transition-matrix
  and web-type updates — the migration is a first-class task, not a footnote;
  (2) the ledger's dead-session detection does not today establish "dead WITH uncommitted
  work" — name the real seam or narrow the claim;
  (3) S4 transitions ONLY an already-associated request (associate-not-create is the law;
  a dead session without a row gets NO fabricated row), correlation key defined
  (session_id vs mapped request id vs work_key), fail-open on every absent dependency;
  (4) new terminal disposition needed plan-wide: blocked/asked rows for obsolete asks have
  no cancel/abandon exit — design the terminal state in the SAME migration as `orphaned`
  so the table rebuilds once.

## Receipts

2026-08-18: S4 is landed, deployed, and accepted at `e54936782eba19541596e73303b818a6a0e47cd4`. The requests table rebuild widens the closed state constraint once for `orphaned` and `canceled` while preserving existing rows, receipt/transition children, their foreign-key declarations, and the partial unique plan index. Recovery uses the ledger sweep's authoritative evidence: an observed runtime classified `FINISHED`, `finishReason: "process exited"`, a positive `rescuedPaths`, and the exact non-null `sessionId`. Classifier `ORPHANED` means no process was observed yet and is only a launch birth-window, so it never moves a request. Absent associations create nothing and reconciliation failure leaves session telemetry available. Generic create/update paths cannot fabricate recovery rows. Explicit reasoned cancellation is terminal, while recovery rows can be claimed again. `/requests` adds owner-language `Needs recovery` and `Closed` surfaces.

Evidence: focused agent-session adapter tests passed 12/12; request-store/dedup/session-adapter tests passed 41/41; request CLI tests passed 33/33; the exact-tree web build passed under the unchanged strict package-metadata policy. Installed acceptance used a real process-backed ledger entry, an exact-session-associated in-flight request, and one dirty file rescued to commit `4e47b5dbb69fe10459c2ee9d9208f876b5cb15fa`. The installed collector moved only that request to recovery. Real browser navigation from `/` to `/requests` showed `Needs recovery 1`, `Worker stopped`, the exact one-file detail, and `Lifecycle acceptance worker`; the drawer said `This request needs recovery`, and the request was absent from Asked, In flight, Blocked — needs you, and Closed. Remaining named gaps: the broad collector suite was not rerun after the focused proof; the complete `RequestsContent.test.tsx` file still has two pre-existing failures caused by duplicate receipt text and duplicate answer textboxes, while the new recovery test passes; release pruning remains degraded by one old invalid retired collector release.

2026-08-16: S2 owner-answer surface implemented. The authenticated mutation endpoint is
`POST /requests/:id/answer`; it requires a trimmed, non-empty `answer` and accepts optional
`next_state`, defaults `blocked_needs_owner -> in_flight`, records actor `owner` and the answer
as the transition reason, and adds `Owner answer: …` to the receipt trail. Blocked board cards
and `/decisions` now use the same multiline answer control; `/decisions` includes board-blocked
rows and its waiting count combines both sources. Factory-projected `factory-decision-<id>` rows
reuse the collector's `factory.decision.answer` action gateway (the same trace-DB seam as
`factory answer`); unavailable or rejected delivery is retained as a named receipt-trail gap,
never guessed. Evidence: `bun test src/requests/routes.test.ts` 25 pass; `bun run typecheck`
clean; `bun test --timeout 20000` run completed after the focused/static checks. Not verified:
the requested `pnpm --filter web test` and `pnpm --filter web typecheck` could not run in this
fresh worktree because its private web workspace dependencies are absent and the normal pnpm
installer stopped at the repository supply-chain verification step; no live collector/factory DB,
deployed web UI, or real owner-browser flow was exercised (local-only, no push).

2026-08-16: plan authored; S1 dispatched to codex (worktree wt/task-lifecycle).

2026-08-16: S1 implemented in worktree `task-lifecycle-s1`. Collector now owns the
`asked -> in_flight -> blocked_needs_owner -> in_flight -> shipped` transition matrix,
resolves exact request IDs or exact work keys, requires an owner-facing blocked reason,
returns the current state on invalid-transition 409s, and records actor/time/reason in a
transition trail. `od-requests block`/`unblock`, the fail-open AskUserQuestion writer, and
the waiting-on-owner Stop check use that seam. Evidence: focused collector 29 pass; CLI
24 pass; AskUserQuestion writer 4 pass; blocked-row Stop cases 3 pass; existing
say-the-word + dispatcher settings 25 pass; existing task-board-sync 15 pass; collector
typecheck clean. Broad `bun test` result: 902 pass, 2 skipped, 1 unrelated pre-existing
failure in `createDeployStatusAdapter > reports queued requests with identity, age, and
observed timestamp` (reproduced once in isolation; stale deploy event fixture state).
Not verified: installed hook/collector runtime and a real owner-board row, because this
slice was explicitly requested as a local commit with no push/deploy.

2026-08-16: session and plan attribution added for board filtering. Request rows now retain
the existing `plan_ref`/`work_key` convention as the plan-slug key plus nullable
`session_name` and `session_id`. The task bridge reads the lightweight Claude session-state
files used by `list-sessions` to resolve a friendly name without its expensive process scan;
missing state remains session-id-only and never blocks a write. `/requests` now filters by
Plan and Session (friendly name, otherwise a short session ID). Evidence: focused store
migration 12 pass and task bridge 17 pass. Not verified: web package test/typecheck in this
worktree because dependencies are not installed; no live collector migration or deployed UI
was touched, per the local-only request.

2026-08-16: docs enforcement extended: pending owner-decision prose in `docs/**/*.md` is now
write-time denied unless its paragraph embeds the blocked board row ID; historical plan/spec
prose is reconcilable on demand with `decision-marker-sweep` (and explicitly registerable with
`--register`). Resolved, dated decision records remain history. Evidence: decision-marker gate
and sweep tests plus dispatcher settings tests. Not verified: installed Claude hook runtime or
real board registration; this local-only slice does not push or deploy.

2026-08-16: decision visibility follow-up implemented. A fresh `asked` request may now enter
`blocked_needs_owner` directly with the existing required actor and owner-language reason;
the review-gated matrix change is EXPLICITLY LIMITED to that one legal edge — the orphaned-state
SQLite table rebuild remains gated and untouched. `od-requests block` documents and exercises
the direct path. The Stop gate now catches owner-decision wording including the incident sentence
“waiting on exactly one thing: your clearance decision” and still fails open when the collector
cannot be read. The scheduled, readonly factory adapter projects pending factory `decisions`
into `factory-decision-<id>` blocked rows, resumes answered decisions, and ships closed/canceled
decision rows through the existing matrix; a missing factory database remains an empty, fail-open
projection. Every entry to `blocked_needs_owner` sends one best-effort `botmaster --channel
Overdeck --needs-answer` notification with title and reason; missing bot configuration is silent.
Evidence: focused request-store + factory adapter tests 46 pass; request routes 21 pass;
`test-say-the-word-blocked-row.mjs` 3 pass; say-the-word suite 19 pass; `od-requests` CLI 27
pass; `bun run typecheck` clean; full `bun test --timeout 20000` exited 0 (two existing live
Kanboard tests skipped). Not verified: a deployed collector/factory database or real Telegram
delivery, because this local-only slice was not pushed or deployed.
