# v0.3 scope: legible takeovers, finished command surface

Owner: `pm-v03`. Basis: [audit.md](./audit.md), [backlog.md](./backlog.md), and
the shipped v0.2.0 tree (verified: `package.json` 0.2.0, 184 tests in 13 files,
`tsc --noEmit` clean).

Version target: **0.3.0**. No new runtime dependency may be added: 0.3.0 is
delivered with the one runtime dependency the package already has (`yaml`) plus
the `node:` standard library. Everything here is code that already exists in
this repository, wired to a surface a person or an Agent can read.

v0.2 made the Board trustworthy: claims are enforced, `rev` is visible, one bad
file no longer blanks the Board. v0.3 is about the moment the Board stops being
polite — a takeover. When `force` is used, the Board records *that* it happened
and never *why*, and the human-facing `/kanban` surface cannot name the tasks or
handles it is supposed to complete. So: make takeovers legible, and finish the
human command surface.

## What the audit and backlog get wrong

The backlog is stale against the shipped tree. Corrections below are the basis
for the scope table, not a rewrite of the prose in `docs/product/backlog.md`.

| Backlog item | Status in v0.2.0 | Evidence |
|---|---|---|
| #1 claim age and `/kanban release` | **Shipped.** | `claimAgeMs`/`claimStart` in `src/store.ts`, `releaseTask`, `/kanban release` in `src/command.ts`, `CHANGELOG.md` 0.2.0. |
| #2 enforce claims in non-active statuses | **Open, deliberately.** | v0.2 scope defers it; `src/store.ts` still gates on `target.active \|\| currentStatus?.active`. Re-deferred for v0.3 — see below. |
| #3 board briefing, `promptGuidelines` | **Shipped.** | `kanban_list {briefing: true}`, `renderBriefing`, per-tool guidelines in `src/index.ts`. |
| #4 bounded listing | **Shipped.** | `limit`/`offset`/`summary` in `src/schemas.ts` and `src/index.ts`. The "print the workflow only when it changed" sub-bullet was **dropped on purpose** — the workflow block follows the workflow's size, not the Board's, so it is already bounded. |
| #5 unreadable task file | **Shipped.** | `readTasksWith` collects `fileErrors`; `validate` reports `unreadable_task`. |
| #6 `/kanban` scale story | **Open, deferred.** | Column head limit, `--json --limit`, sorting: none present. |
| #7 `rev` visibility, `expectedRev` guidance | **Shipped.** | `renderTaskRows` prints `rev`; `stale` errors name observed/expected rev. |
| #8 complete errors | **Shipped**, except `KanbanError.toJSON()` still sits unused beside the shared `fail()` serializer. Cosmetic; not worth a 0.3 task. |
| #9 `reason` on a Forced assignment | **Open.** No `reason` field anywhere. | `rg reason src/{schemas,types,store,render}.ts` is empty; `HistoryEntry` has only `at/action/by/from/to/assignee/forced`. |
| #10 silent third-party assignment | **Open by design.** | `assignTask` records a plain `assign` when `current` is null, whatever the `wanted` handle is. |
| #11 unlocked read in `kanban_move` | **Shipped.** | `moveTask` returns `{...task, from}` from inside `withBoardLock`; `src/index.ts` no longer pre-reads. **Item closed**; its checkboxes should be ticked. |
| #12 finish `/kanban` completions and help | **Half shipped.** | `USAGE` already states the Claim rule, the Terminal-status rule, and the Handle convention (the audit's "states no rules" is out of date). `completeKanbanCommand` still returns no Task IDs and no Handles — the audit's docstring complaint is correct. |
| #13 duplicate Task IDs after a merge | **Half shipped.** | `id_mismatch` and `counter_low` are both in `validateBoard`. `/kanban repair-ids` does not exist. |

Two audit claims are wrong and are recorded here so nobody re-litigates them:
`USAGE` in `src/command.ts` **does** state the three rules (#12), and
`moveTask` **does** read inside the lock (#11).

## Theme

A takeover must leave a readable trace, and every `/kanban` affordance that the
help text promises must actually exist.

## In scope

| Task | Why now |
|---|---|
| <a id="t-0012"></a>**T-0012** — #9 `reason` on a Forced assignment, rendered in `/kanban <task-id>`, flagged by `validate` | `forced: true` alone cannot separate "the owner crashed" from "the owner was preempted" — the one distinction a person auditing a takeover needs. History is already documented as self-reported (`GLOSSARY.md`), so this buys legibility without pretending to buy trust. |
| <a id="t-0013"></a>**T-0013** — #10 third-party assignment requires `force` | Decided, not deferred: ADR 0009 enforces it. Setting an Assignee for someone else is an act the owner never consented to and the Board currently cannot express. The v0.2 objection was that an assignment outside an active status is a hint, not a claim — ADR 0009 answers it (the hint stays a hint; it just needs to be *deliberate*). |
| <a id="t-0014"></a>**T-0014** — #12a `/kanban` completions include Task IDs (status + title) and observed Handles | The docstring promises it, `USAGE` implies it, and `/kanban mine <handle>` and `/kanban <task-id>` are currently pure recall. This is the largest usability gap on the human surface. |
| <a id="t-0015"></a>**T-0015** — #12b `/kanban help` gains the board-location and no-board rules | `USAGE` covers claim/terminal/handle; it says nothing about where completions look for the Board or what happens without one, because that is exactly what completion can now silently get wrong. |
| <a id="t-0016"></a>**T-0016** — #13 `/kanban repair-ids --dry-run` (and `--yes`) | `id_mismatch` detects the conflict but the only fix is hand-editing YAML. A merge on `counter.json` is the documented failure mode (`docs/adr/0005`), and detection without repair makes the Board report a problem it cannot help with. |
| <a id="t-0017"></a>**T-0017** — backlog checkbox hygiene, CHANGELOG 0.3.0, version bump | `[x]` must mean shipped in 0.2.0 and `[ ]` must mean genuinely open, or the next release is planned from fiction; and behavior changes here are user-visible rules, so ADR 0009, `README.md`, `docs/workflow.md`, and the 0.3.0 CHANGELOG section are part of the release, not an afterthought. Runs last. |

## Deliberately deferred

| Backlog item | Decision |
|---|---|
| #2 enforce the Claim in every non-Terminal Status | Deferred again, for the same v0.2 reason: it rewrites the claim model ADR 0008 defines and needs a migration note for boards already sitting in non-active statuses with an assignee. v0.3 makes forced takeovers legible; it does not change when `force` is required. Any implementation must start with a new ADR, not a patch. |
| #6 `/kanban` scale story (column head limit, `--json --limit`, sorting, age) | Deferred. It is real, and a 100-task Board is unpleasant to read, but it is a rendering redesign with no interaction with takeover legibility. `/kanban status`, `/kanban mine`, and `--json` are the documented escapes until then. |
| #8 the last cosmetic (`KanbanError.toJSON()` is unused) | Deferred. Dead code beside a working serializer; no user-visible cost, and touching the error path for tidiness is a poor trade in the same release that touches `validate`. |
| #4's "print the Workflow block only when it changed" | Rejected as a requirement. The workflow block's length follows the workflow, not the task count; suppressing it would make each call's output depend on session history, which is worse for a stateless reader. Already recorded in v0.2; repeated here so it is not reopened. |
| #11 unlocked read in `kanban_move` | Already shipped in 0.2.0. No task; the backlog checkboxes are corrected instead. |
| Renumbering references inside descriptions and comments during `repair-ids` | Out of scope, explicitly. `repair-ids` rewrites the `id` frontmatter and the filename only. A `T-0007` mentioned in prose stays as written; rewriting prose is a text-rewriting problem that can corrupt a task and cannot be verified. The dry-run output is the migration aid. |
| Enforcing the Handle convention (verified Handles, leases, heartbeat expiry) | Rejected. ADR 0004 and ADR 0008 already refuse this: Handles are self-asserted and a claim never expires silently. `reason` and `/kanban release` are the two sanctioned answers to an abandoned claim. |

## Sequencing

- **T-0012, T-0013, T-0016** are independent and may run in parallel. T-0013 is the only one that must land an ADR before code.
- **T-0014 before T-0015**: the help text must describe the completed behavior, not the intended one.
- **T-0017 last**, after every other v0.3 task is `done`; it is the only task that bumps the version.

Work orders live on the Board (T-0012 … T-0017), one per row above; this
document is the scope, the Board is the schedule.

## #10 decision, in full

**Enforce it.** Assigning a Task to a Handle other than the caller's requires
`force: true`, and the resulting History entry is a `force-assign` with
`forced: true`, so it is distinguishable from a self-claim by reading the file.

Why the v0.2 deferral is overruled:

- The v0.2 objection was that a non-active assignment is a "hint". That is still
  true and is preserved — an assignment outside an active status is not a claim
  and is still not enforced on move. What changes is only *who may write the
  hint*: someone may write their own hint, or deliberately write someone else's.
- The current behavior cannot be defended in one sentence: "any handle may
  assign an unassigned task to any other handle, silently". A rule the help text
  would be embarrassing to state is a rule worth changing.
- It closes a real coordination hole with one branch in `assignTask`: two agents
  that both see an unassigned Task can currently resolve the race by assigning
  it to a third party, and nothing in History says so.

This reverses an explicit v0.2 decision, so **ADR 0009 is a prerequisite** and
must record: the rule, the override, the migration impact (boards with historic
third-party `assign` entries stay valid and readable — no data rewrite), and the
rejected alternatives (a config flag; a warning instead of an error).

Consequences to state in the docs: releasing another handle's assignment
already needs `force` (`assignTask`'s `wrong_owner` path), so after ADR 0009 the
rule is uniform — **any write to an Assignee that is not yours, in either
direction, needs `force`** — and it must be said in `kanban_assign`'s
`promptGuidelines` and in `USAGE`.

## #12 constraint: how completions find the Board

`pi.registerCommand`'s `getArgumentCompletions` receives only `argumentPrefix`
(`RegisteredCommand.getArgumentCompletions?: (argumentPrefix: string) => …` in
`@earendil-works/pi-coding-agent`). It receives no `cwd` and no
`ExtensionCommandContext`, so the Board cannot be located the way the handler
locates it (`resolveBoard({ cwd: ctx.cwd })`).

The design therefore must, and the task must require it to:

1. **Resolve the Board from the session, not from the call.** `registerCommand`
   is called inside the extension factory, where `ctx` is available at session
   start and at `session_start`/`cwd` events. Cache the Board `dir` for the
   session (re-resolving on the same events the factory already handles) and let
   completions read that cached location. A completion must never throw and
   never write.
2. **State what happens with no Board.** No cached location, a missing
   `.pi/kanban`, or an error while reading tasks yields the subcommand list
   only — the current helper's behavior. Never an empty list, never an
   exception, never a stale location silently reused across a `cwd` change
   without re-resolution.
3. **Say so in `/kanban help`.** `USAGE` must state that completions use the
   session's Board and are unavailable (subcommands only) when there is none.
4. **Stay read-only.** Completions list Task IDs with status and title and the
   Handles observed in the current assignees and History; they do not filter by
   relevance beyond the prefix, and they never take the board lock.

A test must drive `completeKanbanCommand` with a Board that contains a Task and
assert the Task ID appears, and drive it with no Board and assert the subcommand
list still comes back.

## Release criteria for v0.3

Every bullet below is checkable by running a command in this repository.

- [ ] `npx vitest run` and `npx tsc --noEmit` are clean, and the test count is greater than 184.
- [ ] `npx tsc --noEmit` passes with no change to `package.json` `dependencies` (still exactly `yaml`).
- [ ] `rg -n "reason" src/schemas.ts src/types.ts` shows the new optional field on `kanban_assign` and `kanban_move` and on `HistoryEntry`.
- [ ] After a forced takeover on a scratch Board with `reason: "owner idle 2d"`, `rg -n "reason:" <scratch>/tasks/T-*.md` finds it on the `force-assign` entry, and `renderTask` prints it on the history line.
- [ ] `/kanban validate` reports code `unannotated_forced` for a scratch Board holding a `force-assign` entry with no `reason`, and reports nothing for the same entry with a reason.
- [ ] `kanban_assign {task, assignee: "<other>", agent: "me"}` without `force` fails with code `wrong_owner` and a hint naming `force: true`; the same call with `force: true` succeeds and writes a `force-assign` entry.
- [ ] `npx vitest run` includes a test that a third-party assignment without `force` throws and that the scratch task file is unchanged afterwards.
- [ ] `completeKanbanCommand("T-0")` on a Board with T-0001 returns a label containing `T-0001`; `completeKanbanCommand("")` with no Board returns the subcommand list without throwing.
- [ ] `/kanban help` text includes the words that name the Claim rule, the Terminal-status rule, the Handle convention, and the no-Board completion rule (asserted by a test, not by eye).
- [ ] `/kanban repair-ids --dry-run` on a scratch Board whose `T-0002.md` declares `id: T-0009` prints the rename and writes nothing (task files and `counter.json` byte-identical before and after, asserted by a test).
- [ ] `/kanban repair-ids --yes` on the same scratch Board renames the file and rewrites its frontmatter `id`, and `validate` afterwards reports no `id_mismatch`.
- [ ] After `repair-ids --yes`, `counter.json` still reads greater than the highest existing Task ID and `validate` reports no `counter_low`.
- [ ] `rg -n "id_mismatch|unannotated_forced" README.md docs/workflow.md` finds the documented problem codes.
- [ ] `rg -n "repair-ids" README.md docs/workflow.md src/command.ts` finds the subcommand in the docs and in the handler.
- [ ] `docs/adr/0009-third-party-assignment-is-forced.md` exists, and `rg -n "0009" README.md docs/adr/*.md` references it.
- [ ] `git diff --stat docs/product/backlog.md` touches only `[x]`/`[ ]` markers, never prose.
- [ ] `package.json` version is `0.3.0`, and `CHANGELOG.md` has a `## 0.3.0` section above `## 0.2.0` (release task only; the version is not bumped by the scope tasks).
- [ ] `.pi/kanban/counter.json` `next` is greater than the highest task file on this Board's own `.pi/kanban/tasks`.
