# Session workflow: the five stores and the guards *Last verified: 2026-08-25 @ 5cae33a* 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: @ ` 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 may start without one: filing is not a gate on work (owner's call, 2026-08-25). **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 # 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 One hook in `.githooks/`, tracked, wired with `core.hooksPath`. A fresh clone needs `git config core.hooksPath .githooks`. `commit-msg` and its `(V-)` requirement were deleted on 2026-08-25: a subject ref that names a task nobody filed is a wrong link, not a record. - `pre-commit` refuses master, and refuses more than 300 changed lines in non-markdown files. Markdown is exempt and may land as one batch. 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.