# The work loop (https://docs.vitrinka.ai/pm/work-loop)



## The task is the handoff [#idea]

A task worked by an agent should stay true without the agent thinking about how. Follow-ups, omissions and spotted defects land as children the moment they are seen; status moves with runs and pull requests; and a session's residue reaches the task through one door whose printed output *is* the chat hand-back. The next session pastes the task URL and gets a bounded, server-composed pickup that says what to do, where to start, what to read first, which branch exists and what not to load.

Three moments, three verbs:

| Moment      | Verb          | What the server does                                                    |
| ----------- | ------------- | ----------------------------------------------------------------------- |
| Starting    | **pickup**    | Composes one bounded view of the task and claims it                     |
| Discovering | **spot**      | Files a defect or follow-up as a child, right now                       |
| Ending      | **hand back** | Files next steps, attaches the summary, moves status, returns the block |

Each verb is a skill (`/vitrinka:pickup`, `/vitrinka:spot`, `/vitrinka:handoff` in Claude Code; `$pickup`, `$spot`, `$handoff` in Codex), an MCP call and a CLI command. The [rules](/pm/rules) and the session hooks do the rest.

## Pickup [#pickup]

`get {kind: "task", id, view: "pickup"}` (`vitrinka task pickup <id|url>`) answers with the pickup JSON and `rendered`, a dense text block to print as returned:

```text
pickup #718 · Checkout as one guided flow · epic · in_progress
WHAT   <the epic's outcome, else the body's first line>
LAST   <when> · by <who> · <the last hand-back's summary>
DONE   <what that hand-back finished>
ON     <surfaces it opened — APIs, components> — <where to build on>
BRANCH <branch> · worktree <path>
BEFORE <what must be true first> [#id status] · …
READ   <kind> v<n> <filename> — <hint>          (one line each)
WHO    <actor> since <relative>                  (live runs)
NEXT   ☐ <open step on this task> [type]         (the task's own checklist)
NEXT   [<key>](<url>) <title> [type]             (open children, rank order)
SKIP   17 done children · 42 events · 3 attachments · 5 comments — not loaded
```

Rows the server had nothing for are omitted. An attachment worth knowing about but not worth loading shows under READ as `read on demand: <kind>`. The view has a hard size cap (6,000 bytes): rows past it are dropped whole and counted in SKIP, never truncated mid-row. Done work is never expanded, events and transcripts are never loaded, and the CLI form also says whether the branch exists on origin.

A session works from it in order: **BEFORE** is a stop, not a note; **READ** names the only attachments to load (`read_task_ref`); **BRANCH** is continued when it exists, else a fresh `vt-<id>` worktree binds the session; the task is **claimed** with a live run (`vitrinka task start`, usually done by the SessionStart hook); and the first **NEXT** row nobody in WHO holds is the work. On an epic the NEXT rows are its children — the epic itself is never worked.

A task or epic URL pasted into a prompt means "pickup first" for every MCP client: the server instructions say so, and the full tree is never loaded up front.

## Spot [#spot]

Something seen mid-work that will not be done in this change — a defect, a follow-up, a call a human owns — is filed the moment it is seen, never batched for the end and never left in chat. `spot` (`vitrinka task spot <title>`, `POST /projects/{project}/spot`) files it as a child of the bound task; with no task bound, under the project's rolling **Found during implementation** epic, minted on first use.

* **Typed by nature** — `bug` for a defect that exists today, `task` for a follow-up that needs its own session; a check on the work in hand is a step on the bound task, never a child, and a call a human owns is a `human` gate there (`Decide: …`).
* **A step outgrows the checklist** — `fromStep` promotes it: the item leaves the parent's checklist as the child is filed.
* **Body = what · where · why** — what was seen, where (file, route, screen) and why it is out of scope here.
* **`production: true`** adds the `might-affect-production` label, so a user-visible defect is findable before the next release.
* **Refs ride along** — a board, a pull request or a recorded session as evidence.

The bound task's children are read first so nothing is filed twice.

## Hand back [#handback]

Work on a bound task ends with one call. `hand_back` (`vitrinka task handback [id]`, `POST /tasks/{id}/handback`) takes what would have been written into the chat and lands it on the task in one transaction:

| Field                  | Becomes                                                                                                                                                                                                        |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `summary`              | The next pickup's LAST row — written for the team, not the reviewer                                                                                                                                            |
| `done`                 | One line per finished thing in the block                                                                                                                                                                       |
| `surfaces` · `buildOn` | The pickup's ON row — what exists now and where to start                                                                                                                                                       |
| `next`                 | Steps and gates on the task's checklist, in the order given (`kind` step · human · date · action, `who` / `at`, a `when` that ticks by itself); `task: true` files a child for work that needs its own session |
| `omitted`              | `decide: true` lands as a `human` gate `Decide: …` waiting on `who` — for a run nobody attends; a live session asks the human first and records the answer in `summary`; other omissions are children          |
| `branch` · `worktree`  | The BRANCH row                                                                                                                                                                                                 |
| `prerequisites`        | The BEFORE row; a non-empty list marks the parent epic waiting                                                                                                                                                 |
| `readFirst`            | Ref ids that head the next pickup's READ rows                                                                                                                                                                  |
| `refs`                 | The pull request and the board this work produced                                                                                                                                                              |
| `sessionId`            | The live run bound at session start ends with the summary                                                                                                                                                      |

The summary is stored as the next **version** of the task's `handoff` attachment, so every hand-back stays readable and the pickup always reads the latest. The parent epic's `next_action` moves to the first open child. The reply carries `rendered`:

```text
Summary
<summary>

<one line per done item>

Next steps
1. <open step on this task> — human gate · <who>
2. <step>
Filed 2 child task(s): [<key>](<child url>) [<key>](<child url>)
📋 Task: [<key>](<url>) — <status>
🔀 PR (merged): [<repo>#<n>](<url>)
🖼️ Board: [<board title>](<url>)
```

Every link is a masked Markdown link: a short label (the task key, the pull request's `repo#N`, the board's title) over the exact server-minted URL, so a terminal or chat shows the label and opens the address. Next steps is the task's open checklist — gates first, each human gate with who it waits on; when nothing is open it says so. Children filed with `task: true` or as omissions are one line of links; the next pickup's NEXT rows carry them. A pull request states its status before the link: `merged`, `waiting` (open, not merged yet), `draft` or `closed`; a ref attached without one reads `unknown`.

That block is the hand-back. An agent prints it as returned and adds nothing; a line it feels like adding by hand is a field it forgot to fill.

Status moves by itself and `status` is filled only to correct it — reopening, cancelling, sending back to backlog.

A session's transcript reaches the task only when you ask: `vitrinka task handback --transcript` archives it after the hand-back and labels the task `eve-distill`, so a colleague reads the digest instead of the whole session.

## Status moves by itself [#status]

Three built-in [rules](/pm/rules) read the task ledger's events, so nobody sets status by hand while working (the first two are new with the work loop, the third predates it):

| Rule                  | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `run-starts-work`     | A live run starting on a backlog or unstarted task marks it in progress.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `open-pr-reviews`     | An open pull request that claims the task marks it in review. A PR that only mentions the task in its body links it and never moves its state.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `merged-pr-completes` | A merged pull request that claims the task completes it: the task's `vt-<id>` in the PR's branch or title, a closing keyword in its body (`Closes vt-12`), or a PR attached by hand. It fires once, at the merge or when a person attaches an already-merged PR: a task reopened afterwards stays open through later attachments and PR updates, and it completes only the work nothing else holds: while any checklist item is open (a gate, an action, a step) or another pull request claiming the task is still open, the merge only lands on its ref and the task stays open for whoever finishes the rest. A PR that only mentions the task links it and never completes it, and so does a key or keyword in a PR from a repository outside the ones the project lists. |

Epics are excluded: the portfolio ledger owns their closure. `update {kind: "task", id, status}` remains the explicit door for reopening and corrections.

## Session hooks [#hooks]

`vitrinka setup` shows the exact hook entries before writing them and installs them in Claude Code's `settings.json` and, when Codex is on the machine, the three session hooks in `~/.codex/hooks.json`:

* **SessionStart** — on a `vt-<id>` branch, binds the session to the task as a live run and prints the pickup, so the session starts knowing; on a fresh start or `/clear` it also prints the project's todos whose conditions fired and the schedules that are due.
* **SessionEnd** — writes what git knows into the run: commits since the run started, the pull request on the branch, files touched. A forgotten session still leaves a trail.
* **Stop** — blocks the closing turn only when its final message carries a `Next steps` heading and the bound task has no hand-back newer than this session's run. The reason says "call hand\_back first"; the answer is the handoff skill. It reads the heading only, never the content.
* **PreToolUse** (Claude Code only) — when a file is edited, announces a todo whose `path` condition matches it, once per session.

Codex runs the same three hooks; its Stop event hands the hook the last assistant message directly. Codex trusts a hook once per machine: after `vitrinka setup`, open Codex and run `/hooks` to trust the three entries — until then Codex skips them, silently under `codex exec`. `vitrinka doctor` shows the state as `hooks·codex`, and vitrinka's own headless Codex runs (`vitrinka work watch --exec codex`) pass `--dangerously-bypass-hook-trust` so the gate runs there without the ritual.

```bash
vitrinka task pickup 718                              # the bounded pickup, with the branch check
vitrinka task spot "Checkout total ignores the coupon" --production \
  --body "seen on /checkout with a percentage coupon · cart.ts · out of scope for the address step"
vitrinka task handback --summary "Address step ships as one guided flow" \
  --done "address form validates inline" --next "Wire the payment step" \
  --pr acme/shop#42
```

## Where it happens [#where]

* **Agents** — `get {kind: "task", view: "pickup"}`, `spot`, `hand_back`; the `pickup`, `spot` and `handoff` skills of the `pm` module.
* **The terminal** — `vitrinka task pickup`, `task spot`, `task handback`; `task start` with no id binds the checkout's `vt-<id>` branch.
* **The project page** — the hand-back is the task's latest `handoff` attachment, the children are its subtasks, and the [Overview](/pm/brief) shows what needs you, the sprint timeline and the picked sprint's progress.
