Veri Docs GitHub Download

The dispatcher

In Veri's lifecycle, dispatch is one human gesture: veri dispatch writes the approval stamp and the claim in the same act. The dispatcher is the scheduled half of that gesture — a GitHub Actions recipe that polls veri next and, when the backlog head already carries your approved: stamp, claims it, briefs a headless agent, and ends at a pull request. It spends clearance you granted; it can never grant any. Never a merge — everything above "open a PR" stays yours.

The recipe is one workflow file you copy. It costs nothing while there is nothing to spend: veri next is a deterministic poll, and an empty backlog — or a head still awaiting your judgment — exits before any agent spawns, so scheduled runs on an idle repo spend no tokens.

1. Add the workflow

One file, .github/workflows/veri-dispatch.yml, plus an ANTHROPIC_API_KEY secret for the agent shown (needs @verikb/cli 0.1.3 or newer for veri next / veri dispatch):

name: Veri dispatch

on:
  workflow_dispatch:
  schedule:
    - cron: '17 * * * *'

# One dispatch at a time: overlapping polls would race for the same head.
concurrency:
  group: veri-dispatch
  cancel-in-progress: false

permissions:
  contents: write
  pull-requests: write

jobs:
  dispatch:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: '22'

      - name: Install Veri
        run: npm install -g @verikb/cli

      # veri next prints the backlog head as one tab-separated line —
      # id, title, path — and exits 1 when the backlog is empty. An
      # existing veri/<id> branch means an earlier run already
      # dispatched this work order and its PR is pending review. The
      # stamp guard is the boundary: a fresh approved: stamp is yours
      # to write, never this workflow's — it dispatches a head only
      # when your clearance is already on record.
      - name: Poll the queue
        id: poll
        run: |
          if ! LINE=$(veri next); then
            echo "Backlog empty — exiting without dispatching an agent."
            echo "dispatch=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          IFS=$'\t' read -r ID TITLE WOPATH <<<"$LINE"
          if git ls-remote --exit-code origin "refs/heads/veri/$ID" >/dev/null 2>&1; then
            echo "$ID already dispatched — branch veri/$ID exists, its PR awaits review."
            echo "dispatch=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          if ! grep -q '^approved: ' "$WOPATH"; then
            echo "$ID awaits your dispatch judgment — nothing claimed."
            echo "dispatch=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          {
            echo "dispatch=true"
            echo "id=$ID"
            echo "title=$TITLE"
            echo "wopath=$WOPATH"
          } >> "$GITHUB_OUTPUT"

      # Fail before claiming: a claim with no agent behind it would
      # strand the work order on a branch nobody is working.
      - name: Preflight — an agent credential must exist
        if: steps.poll.outputs.dispatch == 'true'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          if [ -z "$ANTHROPIC_API_KEY" ]; then
            echo "::error::${{ steps.poll.outputs.id }} is cleared for dispatch but no ANTHROPIC_API_KEY secret is configured — nothing was claimed."
            exit 1
          fi

      # veri dispatch flips backlog → in-progress and records the claim —
      # claimed_by / claimed_at — refusing a work order another session
      # holds. The poll step guaranteed the stamp is already on record,
      # so this only spends it: dispatch preserves an existing stamp and
      # never re-dates it. In a maintainers project, set the
      # VERI_DISPATCH_BY repository variable to a listed maintainer and
      # it rides as --by.
      - name: Claim the work order on its own branch
        if: steps.poll.outputs.dispatch == 'true'
        env:
          ID: ${{ steps.poll.outputs.id }}
          WOPATH: ${{ steps.poll.outputs.wopath }}
          VERI_DISPATCH_BY: ${{ vars.VERI_DISPATCH_BY }}
        run: |
          git config user.name "veri-dispatcher"
          git config user.email "veri-dispatcher@users.noreply.github.com"
          git switch -c "veri/$ID"
          CLAIMANT="dispatcher-run-${{ github.run_id }}"
          if [ -n "$VERI_DISPATCH_BY" ]; then
            veri dispatch "$ID" --as "$CLAIMANT" --by "$VERI_DISPATCH_BY"
          else
            veri dispatch "$ID" --as "$CLAIMANT"
          fi
          git add "$WOPATH"
          git commit -m "$ID: started — claimed by $CLAIMANT"

      # The kickoff prompt: name the work order, point at the context
      # package, restate the receipt-and-scope rules. Agent-agnostic —
      # any agent that can run shell commands can brief itself.
      - name: Write the kickoff prompt
        if: steps.poll.outputs.dispatch == 'true'
        env:
          ID: ${{ steps.poll.outputs.id }}
          TITLE: ${{ steps.poll.outputs.title }}
        run: |
          cat > "$RUNNER_TEMP/kickoff.txt" <<EOF
          Implement $ID — $TITLE.
          Before writing any code, fetch the full context package: run \`veri context $ID\`. Follow the workflow rules it opens with, respect the linked decisions, and stay inside the work order's scope.
          File non-trivial choices as proposed decisions — never promote a document yourself. Append a receipt under ## Receipts (date, commit SHA, files touched, one-line summary) and check only the acceptance boxes you have verified.
          If the work order declares a verify: command, run it before writing the receipt — it must exit 0 — and state its outcome in the receipt's sentence. Veri never runs it; you do.
          Commit your work to the current branch with subjects prefixed "$ID:". Do not push, do not merge, and do not approve or promote any document.
          Finish by running \`veri check\` — zero violations is the bar.
          EOF

      # ═══ The agent seam ═══════════════════════════════════════════
      # One step, one contract: a headless agent CLI receives the
      # kickoff prompt and can read files, run commands, and commit.
      # Claude Code is shown; swap the install line and the invocation
      # for any agent CLI with a non-interactive mode — keep the prompt.
      - name: Run the agent
        if: steps.poll.outputs.dispatch == 'true'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          npm install -g @anthropic-ai/claude-code
          claude -p "$(cat "$RUNNER_TEMP/kickoff.txt")" \
            --permission-mode acceptEdits \
            --allowedTools "Bash,Read,Edit,Write,Glob,Grep"

      # The gate evaluates before the PR opens, so the request arrives
      # with its verdict attached.
      - name: Evaluate the gate
        id: gate
        if: steps.poll.outputs.dispatch == 'true'
        continue-on-error: true
        uses: danielyayla/veri@v1

      - name: Open the pull request — never merge
        if: steps.poll.outputs.dispatch == 'true'
        env:
          GH_TOKEN: ${{ github.token }}
          ID: ${{ steps.poll.outputs.id }}
          TITLE: ${{ steps.poll.outputs.title }}
          GATE_OUTCOME: ${{ steps.gate.outcome }}
          GATE_ISSUES: ${{ steps.gate.outputs.issues }}
          GATE_ADVISORIES: ${{ steps.gate.outputs.advisories }}
        run: |
          if [ -n "$(git status --porcelain)" ]; then
            git add -A
            git commit -m "$ID: uncommitted session leftovers, committed by the dispatcher"
          fi
          git push origin "veri/$ID"
          {
            echo "Dispatched from the backlog by [Veri dispatch](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) — recorded clearance spent, never minted."
            echo
            echo "- Work order: $ID — $TITLE"
            echo "- Gate: $GATE_OUTCOME — ${GATE_ISSUES:-?} issue(s), ${GATE_ADVISORIES:-?} advisories"
            echo "- The claim and receipt ride the branch; review, approval, and merge stay human."
          } > "$RUNNER_TEMP/pr-body.md"
          gh pr create \
            --title "$ID: $TITLE" \
            --body-file "$RUNNER_TEMP/pr-body.md" \
            --head "veri/$ID"

2. What a run does

3. Granting clearance

Interactively, dispatch is one gesture: veri dispatch <WO-id> --as <session> writes the stamp and the claim together, and that judgment is yours alone — which is exactly why this workflow refuses to perform it on an unstamped work order. To queue work for scheduled dispatch, grant the clearance yourself first: write approved: YYYY-MM-DD (and approved_by: naming a listed maintainer, in a maintainers project) into the backlog work order's frontmatter and commit it. That is a legal, quiet state — clearance granted but not yet spent — and the next scheduled run spends it. A backlog work order you never stamp is never touched.

4. Swapping the agent

Everything provider-specific lives in the one step marked the agent seam. The contract is small: a CLI that runs non-interactively, receives the prompt, and can read files, execute commands, and commit. To use a different agent, change the install line and the invocation — Codex CLI, Gemini CLI, and their peers all take a prompt and work in a checkout the same way. The kickoff prompt is deliberately agent-neutral: it names the work order and tells the agent to brief itself with veri context, which any tool that can run a shell command can do.

5. Tokens and the PR check

Pull requests created with the workflow's own GITHUB_TOKEN don't trigger other workflows — GitHub's guard against action loops — so your veri-check.yml won't run on the dispatcher's PR by default. The recipe compensates by running the gate inside the dispatch run and writing the verdict into the PR body. If you want the check to appear as a real status on the PR, store a fine-grained personal access token (contents + pull requests, this repo only) as a secret and use it for GH_TOKEN and the checkout instead.

The trust ladder

This recipe is deliberately the lowest rung: scheduled polling in CI, one work order at a time, only clearance you already granted, ending at a PR a human reviews. The next rung is veri watch — a local daemon that spawns one agent per cleared work order, each in its own git worktree — which is a named follow-up, not something this recipe does. There is no rung where merging is automatic: the approval gate (who stamps what) is the product, and the dispatcher exists to feed it, not to bypass it.