Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 106 additions & 28 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,55 +172,133 @@ src/

Returns 503 if database is unreachable.

## OpenClaw Agent Workflow Contract
## Agent Workflow Contract

This section is the source of truth for how an OpenClaw agent should interact with Dispatch. Agents should follow this contract instead of grooming a GitHub Project board.
This section is the source of truth for how any agent (Saffron, or any other harness) interacts with Dispatch. It supersedes all prior workflow guidance.

### Heartbeat lifecycle
### Canonical One-Task Worker Loop

At the **start** of each heartbeat:
Every agent heartbeat follows this loop:

1. **Best-effort `POST /api/sync`** to refresh Dispatch's issue cache. Treat any non-2xx, timeout, or network error as a freshness warning — log it and continue. **Do not fail the heartbeat on a sync failure.**
1. **`GET /api/agents/{agentName}/next-task?lane=normal`** (bearer-auth required). Returns exactly one `AgentTask`. If idle (`shouldRun: false`), stop immediately — do not start the model.
2. **Execute exactly one task.** The task type determines what to do (see Task Types below).
3. **`POST /api/agents/{agentName}/tasks/report`** (bearer-auth required). Report the outcome, then stop.

At the **end** of each heartbeat:
```python
def heartbeat(agent_name, dispatch_url):
auth = {"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"}

2. **`POST /api/agent-runs`** (bearer-auth with `DISPATCH_AGENT_TOKEN`) with run metadata: `agentName`, `runType`, `status`, `startedAt`, `finishedAt`, `summary`, `touchedIssueUrls`.
# Step 1: fetch next task (auth required)
task = get(
f"{dispatch_url}/api/agents/{agent_name}/next-task?lane=normal",
headers=auth,
)

### Reading work
# Step 2: idle check — stop before model work
if not task["shouldRun"]:
return

3. **Read issues from `GET /api/issues`.** Do not query the Postgres cache directly — the API is the contract.
4. **Prefer issues assigned via `agent/<agent-id>` label** if present. If no `agent/*` label exists, pick from **Ready** by default. Agents pick `status/ready` issues — `status/backlog` and unlabeled issues need triage and are excluded from the default queue.
5. **Filter by execution lane** using the `lane` query param on `GET /api/agents/[agentName]/queue` (values: `NORMAL`, `ESCALATED`, `BACKLOG`). By default, BACKLOG issues are excluded from the normal agent queue.
6. **Agents pick from Ready by default.** `status/backlog` or unlabeled issues are not queueable unless triage marks them Ready — they need grooming before being actionable.
7. **Respect execution lane classification** when present: NORMAL issues are the primary queue for agents; ESCALATED issues may require higher-judgment support; BACKLOG issues are not actionable until decomposed.
# Step 3: execute exactly one task
result = execute(task)

### PR review-fix queue
# Step 4: report outcome (auth required)
post(
f"{dispatch_url}/api/agents/{agent_name}/tasks/report",
headers=auth,
json={"taskType": task["type"], "outcome": result["outcome"], **result["metadata"]},
)

8. **PR-fix items take precedence over issue work.** Before consuming from the assignment queue, query `GET /api/agents/[agentName]/queue?lane=normal` — `type: "pr-review-fix"` items appear first in the response array.
9. **For each PR-fix item:** verify the PR is open and authored by the expected bot account, checkout the queued branch, apply minimal changes based on `feedback[]`, validate locally, push to the same branch, then mark via `POST /api/pr-fix-queue/mark` with status `fixed`, `blocked`, `stale`, or `ignored`.
10. **Never open a new PR for a queued PR fix.** Workers only push to the existing branch.
11. **PR-fix queue is the sole source of truth.** Do not use workspace-local scripts (e.g., `pr_fix_queue.py`) or state files (e.g., `.state/pr_fix_queue.json`) for PR-fix orchestration — all queue operations go through Dispatch APIs.
# Stop
```

**Optional preflight sync:** Agents may call `POST /api/sync` before fetching their next task to refresh Dispatch's issue cache. This is a best-effort, out-of-band operation — not required for the worker loop and not something agents depend on before every task. Sync failures should be logged as freshness warnings and must not block task execution.

### Auth Requirements

Both `next-task` and `tasks/report` require bearer token authentication via `DISPATCH_AGENT_TOKEN`. Use the existing Dispatch bearer token model — no new environment variables or auth schemes.

```bash
# Fetch next task
curl -s -H "Authorization: Bearer $DISPATCH_AGENT_TOKEN" \
"$DISPATCH_URL/api/agents/saffron/next-task?lane=normal"

# Report outcome
curl -s -X POST -H "Authorization: Bearer $DISPATCH_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"taskType":"implement","outcome":"pr_opened","repoFullName":"org/repo","issueNumber":42,"pullRequestUrl":"https://github.com/org/repo/pull/50"}' \
"$DISPATCH_URL/api/agents/saffron/tasks/report"
```

### Task Types

The `next-task` endpoint returns one of four task types:

| Type | `shouldRun` | Description |
|------|-------------|-------------|
| `idle` | `false` | No work available. Stop immediately — do not start the model. |
| `implement` | `true` | Work exactly one GitHub issue. Open or update one PR, then stop. |
| `followup-pr` | `true` | Update exactly one existing PR with requested changes, then stop. |
| `groom` | `true` | Triage and enrich exactly one issue (labels, lane, status), then stop. (Use `?mode=groom`) |

### Report Outcomes

### Source of truth
The `tasks/report` endpoint accepts these outcomes:

8. **GitHub Issues and PRs remain the source of truth.** Dispatch's Postgres is a cache; do not write back to it as if it were authoritative.
9. **Do not rely on GitHub Projects.** The Projects board is deprecated for this workflow — group by repository instead.
10. **Do not auto-close issues without explicit evidence of completion.** Dispatch's audit log is not a license to close — a green pipeline, merged PR, or human approval is.
| Outcome | Meaning |
|---------|---------|
| `pr_opened` | A new PR was opened for the issue |
| `pr_updated` | An existing PR was updated with changes |
| `issue_updated` | The issue was updated (labels, body, etc.) |
| `issue_closed` | The issue was closed |
| `blocked` | Work cannot proceed without external input |
| `failed` | The task failed unexpectedly |
| `no_changes_needed` | No action was required |

### Failure modes
### Worker Boundaries

11. **Dispatch failures must not fail the heartbeat.** Sync, agent-run POST, and issue read are all best-effort from the heartbeat's perspective. Log a warning, continue.
12. **Tokens are secrets.** `DISPATCH_AGENT_TOKEN` and `GITHUB_TOKEN` must never be logged, echoed, or persisted to disk.
Workers must respect these constraints:

* **Do not merge PRs.** Workers never merge pull requests.
* **Do not groom unless taskType is `groom`.** Implementation workers do not triage issues.
* **Do not claim another issue after finishing one task.** Report outcome and stop. The next heartbeat fetches the next task.
* **Report outcome and stop.** Every heartbeat executes at most one task.

### Source of Truth

* **GitHub Issues and PRs remain the source of truth.** Dispatch's Postgres is a cache; do not write back to it as if it were authoritative.
* **Do not rely on GitHub Projects.** The Projects board is deprecated for this workflow — group by repository instead.
* **Do not auto-close issues without explicit evidence of completion.** A green pipeline, merged PR, or human approval is required.

### Failure Modes

* **`next-task` failure:** If the endpoint returns an error, log a warning and stop — do not start the model.
* **`tasks/report` failure:** If reporting fails after execution, log a warning and stop. The work was completed; the report is best-effort visibility.
* **Optional sync failure:** Log as a freshness warning. Never block task execution.
* **Tokens are secrets.** `DISPATCH_AGENT_TOKEN` and `GITHUB_TOKEN` must never be logged, echoed, or persisted to disk.

### Auditability

13. **Every state-changing move on Dispatch must produce an AuditLog row.** Operators trace agent activity through `/api/audit`. Drag-and-drop moves on the Kanban board already write audit entries via `POST /api/issues/move`; agents using the same endpoint inherit this behavior.
* Label, lane, and issue state changes that go through Dispatch mutation APIs produce AuditLog entries. Operators trace these through `/api/audit`.
* Task execution reports create AgentRun rows via `tasks/report`.

### Worker execution contract
### Legacy APIs

The following endpoints remain available for internal use and backward compatibility but are **not** the primary agent workflow:

* `POST /api/sync` — optional best-effort cache refresh
* `POST /api/agent-runs` — legacy run ingestion (superseded by `tasks/report`)
* `GET /api/issues` — raw issue listing (superseded by `next-task`)
* `GET /api/agents/{name}/queue` — legacy queue endpoint (superseded by `next-task`)

### Detailed Worker Contract

For the detailed worker execution contract (PR fix queue precedence, duplicate PR avoidance, hard completion gates, branch naming conventions, failure response format), see [docs/worker-execution-contract.md](./docs/worker-execution-contract.md). This supersedes ad-hoc behavior and applies to all agent workers.

### Worker cron prompt migration
### Generic Harness Loop

For harness-agnostic integration examples (curl, OpenClaw, Codex, Claude Code), see [docs/generic-harness-loop.md](./docs/generic-harness-loop.md).

### Worker Cron Prompt Migration

Worker cron prompts have been migrated from GitHub Project board readers to Dispatch queue APIs. For migration details, affected cron jobs, and the deprecation of board-reading scripts, see [docs/worker-cron-prompt-migration.md](./docs/worker-cron-prompt-migration.md).

Expand Down
4 changes: 4 additions & 0 deletions docs/agent-workflow.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Generic Agent Workflow — Dispatch Assignment Layer

> **⚠️ SUPERSEDED** — This document describes the legacy claim/unclaim workflow.
> The canonical agent workflow now uses `GET /api/agents/{name}/next-task` and `POST /api/agents/{name}/tasks/report`.
> See [AGENTS.md](../AGENTS.md) (Agent Workflow Contract) and [docs/generic-harness-loop.md](./generic-harness-loop.md) for current guidance.
>
> **Issue:** [misospace/dispatch#59](https://github.com/misospace/dispatch/issues/59)
> **Date:** 2026-05-16

Expand Down
74 changes: 39 additions & 35 deletions docs/generic-harness-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,13 @@ The report endpoint accepts these outcomes:

```python
def worker_heartbeat(agent_name, dispatch_url):
# Best-effort sync — do not fail on error
try:
post(f"{dispatch_url}/api/sync")
except Exception as e:
log_warning(f"sync failed: {e}")
auth = {"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"}

# Fetch next task
task = get(f"{dispatch_url}/api/agents/{agent_name}/next-task?lane=normal")
# Fetch next task (auth required)
task = get(
f"{dispatch_url}/api/agents/{agent_name}/next-task?lane=normal",
headers=auth,
)

# Exit immediately on idle
if not task["shouldRun"]:
Expand All @@ -65,28 +64,29 @@ def worker_heartbeat(agent_name, dispatch_url):
elif task["type"] == "followup-pr":
result = update_pr(task["pullRequest"], task["reasons"])

# Report outcome
post(f"{dispatch_url}/api/agents/{agent_name}/tasks/report", {
"taskType": task["type"],
"outcome": result["outcome"],
**result["metadata"],
})
# Report outcome (auth required)
post(
f"{dispatch_url}/api/agents/{agent_name}/tasks/report",
headers=auth,
json={"taskType": task["type"], "outcome": result["outcome"], **result["metadata"]},
)

# Stop
```

**Optional preflight sync:** Agents may call `POST /api/sync` before fetching their next task to refresh Dispatch's issue cache. This is a best-effort, out-of-band operation — not required for the worker loop and not something agents depend on before every task. Sync failures should be logged as freshness warnings and must not block task execution.

## Generic Groomer Loop

```python
def groomer_heartbeat(agent_name, dispatch_url):
# Best-effort sync — do not fail on error
try:
post(f"{dispatch_url}/api/sync")
except Exception as e:
log_warning(f"sync failed: {e}")
auth = {"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"}

# Fetch grooming task
task = get(f"{dispatch_url}/api/agents/{agent_name}/next-task?mode=groom")
# Fetch grooming task (auth required)
task = get(
f"{dispatch_url}/api/agents/{agent_name}/next-task?mode=groom",
headers=auth,
)

# Exit immediately on idle
if not task["shouldRun"]:
Expand All @@ -96,12 +96,12 @@ def groomer_heartbeat(agent_name, dispatch_url):
if task["type"] == "groom":
result = groom_issue(task["issue"])

# Report outcome
post(f"{dispatch_url}/api/agents/{agent_name}/tasks/report", {
"taskType": task["type"],
"outcome": result["outcome"],
**result["metadata"],
})
# Report outcome (auth required)
post(
f"{dispatch_url}/api/agents/{agent_name}/tasks/report",
headers=auth,
json={"taskType": task["type"], "outcome": result["outcome"], **result["metadata"]},
)

# Stop
```
Expand All @@ -116,15 +116,17 @@ Each example uses the same Dispatch contract. None implies OpenClaw is required.
DISPATCH="https://dispatch.example.com"
AGENT="saffron"

# Idle check
TASK=$(curl -s "$DISPATCH/api/agents/$AGENT/next-task?lane=normal")
# Idle check (auth required)
TASK=$(curl -s -H "Authorization: Bearer $DISPATCH_AGENT_TOKEN" \
"$DISPATCH/api/agents/$AGENT/next-task?lane=normal")
echo "$TASK" | python3 -c "import sys,json; t=json.load(sys.stdin); sys.exit(0 if t['shouldRun'] else 1)" || exit 0

# Execute task (replace with your model invocation)
# ...

# Report result
# Report result (auth required)
curl -s -X POST "$DISPATCH/api/agents/$AGENT/tasks/report" \
-H "Authorization: Bearer $DISPATCH_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"taskType":"implement","outcome":"pr_opened"}'
```
Expand All @@ -136,7 +138,8 @@ Configure the scheduler to run a one-shot job per heartbeat:
```yaml
schedule: "*/15 * * * *"
command: |
TASK=$(curl -s "$DISPATCH/api/agents/$AGENT/next-task?lane=normal")
TASK=$(curl -s -H "Authorization: Bearer $DISPATCH_AGENT_TOKEN" \
"$DISPATCH/api/agents/$AGENT/next-task?lane=normal")
SHOULD_RUN=$(echo "$TASK" | jq -r '.shouldRun')
[ "$SHOULD_RUN" = "false" ] && exit 0
# Start model, execute task, report result
Expand All @@ -146,16 +149,17 @@ command: |

Manually invoke the endpoint, read the task, and execute:

1. Fetch `GET /api/agents/{name}/next-task?lane=normal`
1. Fetch `GET /api/agents/{name}/next-task?lane=normal` (bearer auth required)
2. If idle, stop
3. Execute the task with your preferred tooling
4. Report via `POST /api/agents/{name}/tasks/report`
4. Report via `POST /api/agents/{name}/tasks/report` (bearer auth required)

### Codex or Claude Code One-Shot

```bash
# One-shot: fetch task, feed to model, report
TASK=$(curl -s "$DISPATCH/api/agents/$AGENT/next-task?lane=normal")
# One-shot: fetch task, feed to model, report (auth required)
TASK=$(curl -s -H "Authorization: Bearer $DISPATCH_AGENT_TOKEN" \
"$DISPATCH/api/agents/$AGENT/next-task?lane=normal")
echo "$TASK" | codex --one-shot
# Report outcome after execution
```
Expand All @@ -165,7 +169,7 @@ echo "$TASK" | codex --one-shot
1. **One task per run:** The harness fetches one task, executes it, reports, and stops. It does not loop inside a single heartbeat.
2. **Idle before model startup:** The `next-task` endpoint is read-only and cheap. Call it before starting the model to avoid wasted compute.
3. **No lease mutation:** Calling `next-task` does not claim or lock any issue. The agent claims the issue as part of executing the task.
4. **Best-effort sync:** Dispatch failures must not fail the heartbeat. Log a warning and continue.
4. **Optional preflight sync:** `POST /api/sync` is optional and out-of-band. Sync failures should be logged as freshness warnings and must not block task execution.

## Source Code

Expand Down
3 changes: 3 additions & 0 deletions docs/harness-idle-first.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Idle-First Harness Checks

> **⚠️ SUPERSEDED** — This content has been consolidated into [docs/generic-harness-loop.md](./generic-harness-loop.md).
> See the Generic Worker Loop and Generic Groomer Loop sections for the current integration pattern.
>
> **Issue:** [misospace/dispatch#399](https://github.com/misospace/dispatch/issues/399)
> **Date:** 2026-06-16

Expand Down
8 changes: 6 additions & 2 deletions docs/worker-execution-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,12 @@ Renovate exclusion applies to issue queue items only, not PR review-fix queue it
## Linking

This contract is referenced from:
- [AGENTS.md](../AGENTS.md) — OpenClaw Agent Workflow Contract section
- [OpenClaw Agent — Dispatch Phase 1 Workflow Contract](./openclaw-agent-mc-workflow.md) — historical pre-cutover reference (marked as archived)
- [AGENTS.md](../AGENTS.md) — Agent Workflow Contract section
- [Generic Harness Loop](./generic-harness-loop.md) — harness-agnostic integration examples

## Relationship to next-task

Workers using the canonical `next-task` endpoint automatically receive PR-fix items before issue work. The `next-task` endpoint handles PR-fix queue precedence, linked PR follow-up detection, and idle checks internally. This contract documents the detailed execution rules that apply regardless of how a worker discovers its task.

---

Expand Down