# Dispatch: a poke when the owner has work waiting

An agent session that went idle doesn't hear that the owner moved work to
ready. The dispatcher closes that gap (board #161): a Claude session the
owner runs on `/loop` asks the board, each tick, which sessions to poke,
sends each one short message, and records it.

It is **off by default** and switched on per project, on the board, by a
person signed in: project settings › Dispatch names the session to poke,
how often (`dispatchEvery`, minutes, default 60) and how many times a day
(`dispatchCap`, default 4). Each poke starts a paid turn, so those are cost
controls. A token can turn dispatch off, never on.

A poke is due when all of these hold:
- a ready task is waiting that **a person** moved to ready (an agent's own
  move doesn't count, so a poke never approves on the owner's behalf), that
  isn't a question, and that nobody is on;
- it was moved there **since the last poke** (no nagging about the same
  work);
- the last poke is older than the interval, and today's are under the cap.

`planDispatch(store)` says, per project with dispatch on, whether a poke is
due and why not when it isn't; it reads only. `recordPoke(store, project)`
writes the poke as a comment on the task (`dispatch: poked …`), so every
poke is on the board: who was poked, about what, when.

```js
import { MemoryLog, VirtaStore, planDispatch, recordPoke } from 'tosijs-virta'

const store = new VirtaStore({ log: new MemoryLog(), identity: 'owner' })
await store.load()
await store.setProject('demo', { enrolled: 'today', dispatch: 'demo-session' })
await store.create({ title: 'port the tests', tags: ['project:demo', 'status:ready'] })
const [before] = planDispatch(store)
await recordPoke(store, 'demo')
const [after] = planDispatch(store)
preview.textContent = [before.due, before.text, after.due, after.why].join('\n')
```
```test
test('a poke is due once, then not until something new is ready', () => {
  const [due, text, again, why] = preview.textContent.split('\n')
  expect(due).toBe('true')
  expect(text).toContain('#1 "port the tests"')
  expect(again).toBe('false')
  expect(why).toBe('nothing new moved to ready since the last poke')
})
```
