vitrinka.docs

The work loop

The task is the handoff — a session picks a task up from one bounded view, files what it spots the moment it sees it, and hands back through one door whose printed block is the hand-back.

The task is the handoff

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:

MomentVerbWhat the server does
StartingpickupComposes one bounded view of the task and claims it
DiscoveringspotFiles a defect or follow-up as a child, right now
Endinghand backFiles 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 and the session hooks do the rest.

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:

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

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

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:

FieldBecomes
summaryThe next pickup's LAST row — written for the team, not the reviewer
doneOne line per finished thing in the block
surfaces · buildOnThe pickup's ON row — what exists now and where to start
nextSteps 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
omitteddecide: 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 · worktreeThe BRANCH row
prerequisitesThe BEFORE row; a non-empty list marks the parent epic waiting
readFirstRef ids that head the next pickup's READ rows
refsThe pull request and the board this work produced
sessionIdThe 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:

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

Three built-in 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):

RuleWhat it does
run-starts-workA live run starting on a backlog or unstarted task marks it in progress.
open-pr-reviewsAn 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-completesA 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

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.

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

  • 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 shows what needs you, the sprint timeline and the picked sprint's progress.

On this page