doc/TASK-WATCHDOG.md
A task watchdog is an agent you assign to verify a stopped issue tree and put it back into motion when stopping was a mistake. You configure it on a single issue, and it watches that issue plus its non-watchdog descendants. When every leaf in that subtree comes to rest — done, cancelled, blocked, in review, or waiting on an interaction — and there is no live continuation path, Paperclip wakes the watchdog agent to read the evidence and decide whether the stop is legitimate.
Watchdogs are opt-in per issue. There is no global "watch everything" mode.
Agents can stop work for the wrong reasons: misreading a blocker, accepting a stale plan confirmation, declaring "done" without proof, leaving an issue in_review with no real reviewer, or running into a recoverable failure and giving up. None of those failures wake anyone on their own — the tree just sits.
A task watchdog gives you a second pass on stopped work without rerunning the original assignee. It is verification-shaped, not execution-shaped: the watchdog reads what other agents claimed, checks it against the evidence in the thread, and either accepts the stop or restores a live path.
It is not an output-silence monitor for active runs. That is a separate mechanism — the silent active-run watchdog described in doc/execution-semantics.md §12. Task watchdog only fires when the whole watched subtree has come to rest.
Three concepts share the word "watchdog" inside Paperclip. Keep them separate:
| Concept | What it watches | When it fires |
|---|---|---|
| Task watchdog (this doc) | A configured issue + its non-watchdog descendants | The whole watched subtree has stopped and the stop is new |
| Silent active-run watchdog | A single still-running process | The process has produced no output for the threshold window |
| Liveness recovery | Any agent-owned in_progress issue with no live path | Stalled work detected during the periodic recovery scan |
The task watchdog is configured by you (or by an agent on your behalf). The other two run automatically on every project.
A watchdog has three fields:
| Field | Required | Notes |
|---|---|---|
| Watched issue | yes | The issue you attach the watchdog to. Configured implicitly via the issue you edit. |
| Watchdog agent | yes | Any same-company, invokable agent. Cannot be paused, terminated, or budget-blocked. |
| Instructions | no | Free-form text (trimmed; empty becomes null). Can narrow focus; cannot expand authority. |
A single watched issue holds at most one active watchdog. Re-assigning the agent or editing instructions invalidates the previously reviewed state and forces a fresh evaluation on the next scan.
Two surfaces edit the watchdog:
Set watchdog; configured state shows the watchdog agent icon and name plus a truncated instructions preview. Click the row to open the editor popover with an agent picker, an instructions textarea ("What should the watchdog watch for and how should it keep work moving?"), a Remove button, and a Set watchdog / Update button. When a watchdog run has produced a child review task, the row shows a small badge linking to that task.GET /api/issues/:issueId/watchdog
PUT /api/issues/:issueId/watchdog { "agentId": "...", "instructions": "..." | null }
DELETE /api/issues/:issueId/watchdog
PUT is upsert. DELETE disables the row (it is not hard-deleted; the table keeps the history for audit). All three routes require write access to the watched issue and produce activity records (issue.watchdog_created, issue.watchdog_updated, issue.watchdog_removed) with the run id and actor.
Paperclip runs a watchdog reconciliation tick at server startup, at the end of each heartbeat cycle, and on demand after any mutation that could change the watched subtree (status, blockers, assignment, interactions). The tick is per-company and only walks active rows.
For each active watchdog the tick:
parent_id downward, excluding every issue whose originKind = 'task_watchdog' and everything below it. This excludes the watchdog's own review tasks so it cannot trigger itself.queued, running, scheduled_retry), a queued wake request, or a scheduled retry, the subtree is live and the watchdog does not fire.lastReviewedFingerprint. Match → suppress (the watchdog already saw this exact stopped state). New → proceed.originKind = 'task_watchdog' and originId = watchedIssueId. Idempotent per watchdog — only one review task is ever live at a time.wakeReason = task_watchdog_stopped_subtree, the stop fingerprint, the leaf summaries, the default mandate, and any custom instructions. The idempotency key is (watchdogId, stopFingerprint), so retries cannot stack duplicate wakes.When the subtree changes between scans (someone restarts work, adds a blocker, or accepts an interaction) the stop fingerprint changes too, and the watchdog will be woken again for the new state — even if the previous run already disposed of an earlier fingerprint.
On wake, the watchdog agent reads a fixed default mandate plus your custom instructions. The mandate explicitly tells it to:
The mandate also enforces safety constraints that custom instructions cannot override:
The formal authority contract (the full list of allowed and disallowed mutations, and the eligibility test for accepting plan confirmations) is in doc/SPEC-implementation.md §9.9.
Custom instructions are most useful when they tell the watchdog what evidence to look at and what shortcuts to refuse. Examples:
Before accepting any leaf as done, check that there is a corresponding green CI run linked in the comments. If there isn't, reopen the leaf and ask for one.
Do not accept a
request_confirmationplan that proposes more than five subtasks without first asking me to review. Leave the issue in review and ping me.
If a leaf is blocked on the marketing team, accept the wait but make sure the unblock owner is named in the blocker reason.
What custom instructions cannot do: grant authority outside the watched subtree, approve board-level decisions, expand the interaction kinds the watchdog can resolve, or override safety constraints. The server enforces this regardless of what the instructions say.
Every watchdog-originated mutation is gated by a server-side scope check derived from the agent run's contextSnapshot.taskWatchdog field. The check resolves to a { kind: "watchdog", watchdogId, companyId, watchedIssueId, watchdogIssueId } envelope and rejects:
request_confirmation plan confirmations (see SPEC §9.9 for eligibility)The check is wired into the issue update, status change, blocker, assignment, and interaction routes. Any disallowed mutation is rejected at the route layer; the watchdog agent must take a different path (comment, in-subtree follow-up issue, leave a valid waiting state, escalate to a human owner).
Watchdog-generated review tasks carry originKind = 'task_watchdog' and originId = watchedIssueId. The UI surfaces this in three places:
These origin markers are also what excludes the review task from future scans. The walk-down ignores them, so the watchdog cannot scan or trigger on its own review tasks.
A task watchdog is useful when:
It is not the right tool for:
If what you actually want is "wake me when this is done," use a routine or an issue-thread interaction with continuationPolicy: wake_assignee, not a watchdog.
| Topic | File |
|---|---|
| Authority contract (formal) | doc/SPEC-implementation.md §9.9 |
| Execution semantics (formal) | doc/execution-semantics.md §11 |
| Silent active-run watchdog | doc/execution-semantics.md §12 |
| Database schema | packages/db/src/schema/issue_watchdogs.ts |
| Server service | server/src/services/task-watchdogs.ts |
| Scope enforcement | server/src/services/task-watchdog-scope.ts |
| Wake context + default mandate | packages/adapter-utils/src/server-utils.ts (WATCHDOG_DEFAULT_MANDATE) |
| HTTP routes | server/src/routes/issues.ts (GET/PUT/DELETE /issues/:id/watchdog) |
| Properties UI | ui/src/components/IssueProperties.tsx (Watchdog row) |
| New-issue dialog UI | ui/src/components/NewIssueDialog.tsx |
And that's all folks!
,.
(_|,.
,' /, )_______ _
__j o``-' `.'-)'
(") \'
`-j |
`-._( /
|_\ |--^. /
/_]'|_| /_)_/
/_]' /_]'