From 16a7f27aaf6f2cd5961ec36bc66edd117a1f901f Mon Sep 17 00:00:00 2001 From: Jory Irving Date: Wed, 17 Jun 2026 11:02:26 -0600 Subject: [PATCH 1/2] docs: reconcile agent workflow contract --- AGENTS.md | 132 ++++++++++++++++++++++++------ docs/agent-workflow.md | 4 + docs/generic-harness-loop.md | 62 ++++++++------ docs/harness-idle-first.md | 3 + docs/worker-execution-contract.md | 8 +- 5 files changed, 156 insertions(+), 53 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f441acf..1ebbe94 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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.** +2. **`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. +3. **Execute exactly one task.** The task type determines what to do (see Task Types below). +4. **`POST /api/agents/{agentName}/tasks/report`** (bearer-auth required). Report the outcome, then stop. + +```python +def heartbeat(agent_name, dispatch_url): + # Step 1: best-effort sync + try: + post(f"{dispatch_url}/api/sync") + except Exception as e: + log_warning(f"sync failed: {e}") + + # Step 2: fetch next task (auth required) + task = get( + f"{dispatch_url}/api/agents/{agent_name}/next-task?lane=normal", + headers={"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"}, + ) + + # Step 3: idle check — stop before model work + if not task["shouldRun"]: + return + + # Step 4: execute exactly one task + result = execute(task) + + # Step 5: report outcome (auth required) + post( + f"{dispatch_url}/api/agents/{agent_name}/tasks/report", + headers={"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"}, + json={"taskType": task["type"], "outcome": result["outcome"], **result["metadata"]}, + ) + + # Stop +``` + +### 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 -At the **end** of each heartbeat: +The `next-task` endpoint returns one of four task types: -2. **`POST /api/agent-runs`** (bearer-auth with `DISPATCH_AGENT_TOKEN`) with run metadata: `agentName`, `runType`, `status`, `startedAt`, `finishedAt`, `summary`, `touchedIssueUrls`. +| 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`) | -### Reading work +### Report Outcomes -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/` 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. +The `tasks/report` endpoint accepts these outcomes: -### PR review-fix queue +| 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 | -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. +### Worker Boundaries -### Source of truth +Workers must respect these constraints: -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. +* **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. -### Failure modes +### Source of Truth -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. +* **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 + +* **Dispatch failures must not fail the heartbeat.** Sync, next-task, and report are all best-effort. Log a warning, continue. +* **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. +Every state-changing move on Dispatch must produce an AuditLog row. Operators trace agent activity through `/api/audit`. + +### Legacy APIs + +The following endpoints remain available for internal use and backward compatibility but are **not** the primary agent workflow: -### Worker execution contract +* `POST /api/sync` — best-effort cache refresh (still used at heartbeat start) +* `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). diff --git a/docs/agent-workflow.md b/docs/agent-workflow.md index 4ef4152..6b75f42 100644 --- a/docs/agent-workflow.md +++ b/docs/agent-workflow.md @@ -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 diff --git a/docs/generic-harness-loop.md b/docs/generic-harness-loop.md index 67ed656..40488ae 100644 --- a/docs/generic-harness-loop.md +++ b/docs/generic-harness-loop.md @@ -46,14 +46,19 @@ The report endpoint accepts these outcomes: ```python def worker_heartbeat(agent_name, dispatch_url): + auth = {"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"} + # 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}") - # 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"]: @@ -65,12 +70,12 @@ 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 ``` @@ -79,14 +84,19 @@ def worker_heartbeat(agent_name, dispatch_url): ```python def groomer_heartbeat(agent_name, dispatch_url): + auth = {"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"} + # 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}") - # 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"]: @@ -96,12 +106,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 ``` @@ -116,15 +126,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"}' ``` @@ -136,7 +148,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 @@ -146,16 +159,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 ``` diff --git a/docs/harness-idle-first.md b/docs/harness-idle-first.md index 07b7801..b74a5b2 100644 --- a/docs/harness-idle-first.md +++ b/docs/harness-idle-first.md @@ -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 diff --git a/docs/worker-execution-contract.md b/docs/worker-execution-contract.md index e7cfc61..3fb11c5 100644 --- a/docs/worker-execution-contract.md +++ b/docs/worker-execution-contract.md @@ -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. --- From e20d019da2bcaa52bf5313a16c7326cc16e88a99 Mon Sep 17 00:00:00 2001 From: Jory Irving Date: Wed, 17 Jun 2026 11:50:08 -0600 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20fix=20canonical=20loop=20=E2=80=94?= =?UTF-8?q?=20next-task=20first,=20sync=20optional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 36 ++++++++++++++++++------------------ docs/generic-harness-loop.md | 16 +++------------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1ebbe94..140ed79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,42 +180,39 @@ This section is the source of truth for how any agent (Saffron, or any other har 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.** -2. **`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. -3. **Execute exactly one task.** The task type determines what to do (see Task Types below). -4. **`POST /api/agents/{agentName}/tasks/report`** (bearer-auth required). Report the outcome, then stop. +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. ```python def heartbeat(agent_name, dispatch_url): - # Step 1: best-effort sync - try: - post(f"{dispatch_url}/api/sync") - except Exception as e: - log_warning(f"sync failed: {e}") + auth = {"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"} - # Step 2: fetch next task (auth required) + # Step 1: fetch next task (auth required) task = get( f"{dispatch_url}/api/agents/{agent_name}/next-task?lane=normal", - headers={"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"}, + headers=auth, ) - # Step 3: idle check — stop before model work + # Step 2: idle check — stop before model work if not task["shouldRun"]: return - # Step 4: execute exactly one task + # Step 3: execute exactly one task result = execute(task) - # Step 5: report outcome (auth required) + # Step 4: report outcome (auth required) post( f"{dispatch_url}/api/agents/{agent_name}/tasks/report", - headers={"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"}, + 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. + ### 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. @@ -274,18 +271,21 @@ Workers must respect these constraints: ### Failure Modes -* **Dispatch failures must not fail the heartbeat.** Sync, next-task, and report are all best-effort. Log a warning, continue. +* **`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 -Every state-changing move on Dispatch must produce an AuditLog row. Operators trace agent activity through `/api/audit`. +* 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`. ### Legacy APIs The following endpoints remain available for internal use and backward compatibility but are **not** the primary agent workflow: -* `POST /api/sync` — best-effort cache refresh (still used at heartbeat start) +* `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`) diff --git a/docs/generic-harness-loop.md b/docs/generic-harness-loop.md index 40488ae..59a9d50 100644 --- a/docs/generic-harness-loop.md +++ b/docs/generic-harness-loop.md @@ -48,12 +48,6 @@ The report endpoint accepts these outcomes: def worker_heartbeat(agent_name, dispatch_url): auth = {"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"} - # 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}") - # Fetch next task (auth required) task = get( f"{dispatch_url}/api/agents/{agent_name}/next-task?lane=normal", @@ -80,18 +74,14 @@ def worker_heartbeat(agent_name, dispatch_url): # 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): auth = {"Authorization": f"Bearer {DISPATCH_AGENT_TOKEN}"} - # 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}") - # Fetch grooming task (auth required) task = get( f"{dispatch_url}/api/agents/{agent_name}/next-task?mode=groom", @@ -179,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