Skip to content

Agent dispatch — workflow templates and eligibility precheck

Operational guide for briefing and dispatching Claude Code agents against fork-local backlog items. Implements ADR-0355; see Research-0091 for the design rationale (which Symphony shapes were adopted, which were deliberately dropped).

This page covers three things, in dispatch order:

  1. Pick a workflow template under .claude/workflows/.
  2. Run the eligibility precheck to confirm the work isn't already done or already in flight.
  3. Brief the agent by filling the template's {{...}} slots.

1. Workflow templates

Every recurring agent task class has a template under .claude/workflows/:

File Use it when
_template.md Generic skeleton. Clone for any new task class.
codeql-alert-sweep.md Bulk-fix N CodeQL alerts in a single category.
simd-port.md Port or audit a SIMD path (AVX-512 widening, NEON sister, AVX2 audit).
feature-extractor-port.md Port a feature extractor to a GPU backend (CUDA / SYCL / HIP; the Vulkan backend was removed, ADR-0726).

Each template starts with typed YAML front matter (Symphony §4.1.2 / §4.1.3 shape) that captures the dispatch contract:

name: codeql-alert-sweep
description: Bulk-fix CodeQL alerts in one category
agent_type: general-purpose
isolation: worktree
worktree_drift_check: true
required_deliverables: [changelog, rebase_note]
verification:
  netflix_golden: true
  cross_backend_places: 4
  meson_test: true
forbidden:
  - modify_netflix_golden_assertions
  - lower_test_thresholds
master_status_check: true
backlog_id: T-RC1-MASTER-GREEN

The body below the front matter is the prompt template. {{...}} markers are placeholder slots filled at dispatch time (e.g. {{ALERT_LIST}}, {{BACKLOG_ID}}). The body is plain Markdown — no DSL, no execution engine — so it can be re-edited by hand without touching tooling.

Adding a new template

  1. Copy _template.md to .claude/workflows/<task-class>.md.
  2. Edit the front matter — at minimum, set name to the file's basename, write a one-line description, and decide which required_deliverables and forbidden fields apply.
  3. Replace the body's {{TASK_BODY}} and {{DELIVERABLES_LIST}} placeholders with task-specific guidance.
  4. Land the new template in the same PR as the first dispatch that uses it — never ship a template without a worked example.

2. Eligibility precheck — agent-eligibility-precheck.py

Must run before every dispatch

The script lives at scripts/ci/agent-eligibility-precheck.py.

It runs three checks:

  1. BACKLOG row not closed. Parses .workingdir/BACKLOG.md via scripts/lib/backlog_tracker.py. If the row's status is DONE, CLOSED, REMOVED, BLOCKED or DEFERRED, exit 1.
  2. No merged PR mentions this scope. Calls gh pr list --search "<id> in:title,body" --state merged. Any hit means the work has likely already shipped: exit 1 and list the matching PRs.
  3. No in-flight agent on the same scope. Scans /tmp/claude-<uid>/*/tasks/*.output (the harness's per-task metadata) plus the open-PR head-branch list for any active task that mentions the scope. If found, exit 1.

The gate fails closed. A missing backlog row, unreadable harness task, failed GitHub query, missing gh, or unavailable authentication produces a blocking verdict. Use --task-tag for deliberately untracked work. The two --skip-* flags are explicit offline overrides; without one, an incomplete check never becomes an eligible dispatch.

The backlog is private

.workingdir/BACKLOG.md is private and gitignored, so a fresh clone has no backlog. Without one, --backlog-id blocks dispatch; use --task-tag for the work instead.

Backlog schema

The local backlog uses an explicit checklist schema. Every checkbox must carry a stable backtick ID immediately after it:

2. [ ] `T-RC1-MASTER-GREEN` **Master green and the queue drained**
7. [ ] `T-RC2-BENCH-TUNE` **[BLOCKED]** **Bench and tune**
1. [x] `T-RC1-RELEASE-PIPELINE` **Release pipeline correct and idle**

[x] means DONE; unchecked rows default to OPEN and may start their title with **[BLOCKED]**, **[DEFERRED]** or **[IN_FLIGHT]**. Wrapped, indented lines remain part of the item.

These are schema errors that block dispatch:

  • a checkbox without a stable ID, or a duplicate ID;
  • an unknown explicit marker, or a marker that contradicts the checkbox;
  • an existing backlog with no tracked items.

The parser never invents an ID from list order, title text or a GitHub issue number. Retired pipe-table rows remain readable during migration. See ADR-1303.

Reading the verdict

Verdicts go to stderr in GitHub Actions ::error format so a wrapping CI script can parse them. A closed item fails:

$ python3 scripts/ci/agent-eligibility-precheck.py --backlog-id T-RC1-RELEASE-PIPELINE --skip-gh-search --skip-active-scan
agent-eligibility-precheck: scope=T-RC1-RELEASE-PIPELINE
::error title=agent-eligibility: T-RC1-RELEASE-PIPELINE is DONE::BACKLOG.md row already closed (PRs: none recorded). Title: Release pipeline correct and idle
agent-eligibility-precheck: VERDICT=FAIL — do not dispatch.

$ echo "exit=$?"
exit=1

An open item passes:

$ python3 scripts/ci/agent-eligibility-precheck.py --backlog-id T-RC1-MASTER-GREEN --skip-gh-search --skip-active-scan
agent-eligibility-precheck: scope=T-RC1-MASTER-GREEN
  backlog: T-RC1-MASTER-GREEN status=OPEN priority=None — OK
agent-eligibility-precheck: VERDICT=PASS — dispatch eligible.

$ echo "exit=$?"
exit=0

Flags

Flag Meaning
--backlog-id ID Run all three checks against the given BACKLOG row.
--task-tag TAG Free-form scope tag for runs without a backlog row (e.g. codeql-cpp-overflow). Skips checks 1 and 2.
--skip-gh-search Skip check 2 (merged-PR search). Useful offline.
--skip-active-scan Skip check 3 (in-flight scan).
--repo OWNER/REPO GitHub repository (default: VMAFx/vmafx).
--harness-tasks-glob GLOB Override the harness task-files glob (default: /tmp/claude-<uid>/*/tasks/*.output).
--backlog-path PATH Override the BACKLOG.md path (default: autodetect from the worktree's main repo).

Wiring it in

The Claude Code harness does not currently expose a pre-Agent dispatch hook in settings.json. Until it does, calling the precheck is manual — it sits at the top of every workflow template's body. When the harness gains the hook, register the precheck via .claude/settings.json and remove the manual call.

3. Tracker abstraction — scripts/lib/backlog_tracker.py

The precheck and any future state-audit script consume BACKLOG.md through a typed module rather than re-grepping the file:

from lib.backlog_tracker import BacklogTracker, GitHubTracker, BacklogItem

bk = BacklogTracker()                    # autodetects .workingdir/BACKLOG.md

bk.list_open()                           # -> list[BacklogItem]
bk.list_in_flight()                      # -> list[BacklogItem]
bk.list_closed()                         # -> list[BacklogItem]
bk.get("T-RC1-MASTER-GREEN")            # -> BacklogItem | None

gh = GitHubTracker(repo="VMAFx/vmafx")
gh.merged_prs_since(some_datetime)       # -> list[dict]
gh.search_prs("T-RC1-MASTER-GREEN in:title,body")  # -> list[dict]
gh.open_agent_branches()                 # -> list[str]

BacklogItem exposes:

@dataclass
class BacklogItem:
    id: str            # "T-RC1-MASTER-GREEN"
    title: str         # markdown-stripped first cell
    status: str        # OPEN | IN_FLIGHT | DONE | CLOSED | REMOVED | BLOCKED | DEFERRED
    priority: int | None   # tier number (T0=0, T7=7, TA-* = None)
    pr_refs: list[int]     # PR numbers parsed from "PR #N" markers
    raw_row: str           # original markdown row, for diagnostics

The module is read-only — never writes to BACKLOG.md. Edits remain a manual editorial task per the global "Read AND update local state files" rule. If .workingdir/ ever migrates off Markdown (JSON, SQLite, Linear), this module is the only file that needs to change.

See also