# Session workflow: the five stores and the guards

*Last verified: 2026-08-11 @ 557f5a3*

How a session starts, where each kind of writing belongs, and what the hooks
refuse. `CLAUDE.md` carries the commands. This file carries the reasoning.

## Five stores

Each owns something the others must not hold.

| Store | Holds | Lifetime |
|---|---|---|
| Vikunja task | goal, constraints, assumption ledger, status | durable |
| `CLAUDE.md`, `AGENTS.md` | what an agent must know before touching code | durable |
| `docs/` | design, measurements, decisions | durable |
| `TASK.md` | the brief for this branch, written by `task start`, immutable | one branch |
| `HANDOFF.md` | only what the next agent needs to resume | one session |

`TASK.md` and `.task/` are excluded through `.git/info/exclude`. `HANDOFF.md` is
gitignored and injected at session start. If a line in the handoff would still
matter next week, it is in the wrong file.

## Doc tiers

Tiered by path, so staleness is visible from the filename.

- Files directly under `docs/` are living. They carry a
  `Last verified: <date> @ <sha>` line and are corrected in place.
- Files under `docs/evals/` are dated measurements and are never edited after
  the day. A newer number is a new file, not an edit.
- Files under `docs/archive/` are dead and read by nobody by default.

## Vikunja

This repo is project **Maven** (ID 2). MCP at `http://localhost:9100/mcp`, or
`http://192.168.1.104:9100/mcp` from workpc. Feature, bug and deploy tasks go
there.

A task holds the goal, the constraints and the assumption ledger. A session
without a task id cannot be resumed by anyone, so a session with none asks for
one first.

**Close a finished task with `done: true` and nothing else** (owner's call,
2026-08-07). Do not write a completion summary into the description on the way
out. It is lost anyway, and the durable record is the commit messages and the
merged PR. `update_task` carrying a `description` resets `done` to false, which
is why a write-up ever took two calls.

## The branch tool

`~/.local/bin/task` owns the branch, the commit identity and the PR. One task,
one session, one PR.

```sh
task start <vikunja-id>   # branch off origin/master, write TASK.md, fetch review comments
task pr                   # push, open or refresh the PR, label Vikunja, notify
task comments             # re-pull this branch's review comments into .task/
```

`/pickup` opens a session and `/wrap` closes it. Wrap at roughly half context
rather than letting the session compact.

## Guards

Two hooks in `.githooks/`, tracked, wired with `core.hooksPath`. A fresh clone
needs `git config core.hooksPath .githooks`.

- `pre-commit` refuses master, and refuses more than 300 changed lines in
  non-markdown files. Markdown is exempt and may land as one batch.
- `commit-msg` requires the subject to end with `(V-<id>)`. `V-` and not `#`,
  because Gitea autolinks `#123` to a Gitea issue, which is the wrong tracker.

Two more guards live outside the repo, in `~/.claude/hooks/`. `diff-budget.sh`
blocks further edits past 600 changed lines on a `task/` branch.
`prose_lint_hook.py` checks prose on every write. Both measure against
`origin/master`, so a local master that is ahead of the remote makes the diff
budget read high.

`--no-verify` exists. Using it means saying why in the commit body.

## Static gates

Three analyzers, one target each, and `make analyze` for all three. The
2026-08-10 audit asked for them because none was installed on the box and
`make audit` is a git-grep inventory, not analysis. Do not read `make audit` as
a gate.

- `make vuln`, govulncheck over `./...` (V-682).
- `make lint`, staticcheck over `./...` (V-694).
- `make deadcode`, deadcode with `-test` over `./...` (V-694).

None of the three joins `make test`. All three install over the network, and
`test` has to pass on a box with no route out. `vuln` reads the advisory
database at run time as well. Run `make analyze` before a dependency or
toolchain bump lands, and before calling a symbol unreachable.

Each tool is pinned in the Makefile beside `GO_VERSION`. A gate that moves on
its own is not a gate. Each installs into `deps/bin`, because a tool is not a
dependency of the module.

**staticcheck and deadcode pass against a baseline, not against zero.** The
accepted findings live in `scripts/analyzers/*.baseline`, one line each. A key
holds file, check id and message, never a line number. A line number goes stale
on the next edit above it. The output then reports moved findings as new ones,
and the reader learns to skip it.

`scripts/analyzer-gate.sh` gives the verdict. A finding absent from the baseline
fails. So does a baseline entry whose finding is gone, which is what stops the
accepted set from outliving the repo. Deleting the entry is part of each fix.

`deadcode` runs with `-test` because a test is a caller. Without the flag the
report is 172 lines, most of `internal/router/eval`, none of it a mistake.

A baseline entry carries the reason it stays. Three reasons appear. Another task
owns the finding. The check cannot see through a false positive. A cosmetic
finding waits for a sweep.
