Task lifecycle: filed to verified¶
The board's core invariant: lifecycle (backlog/ready/in_progress/blocked/
cancelled/done) is agent- and human-writable, but verified is written
only by the deterministic checker sweep from evidence it can re-derive
from source of truth (a merged PR, a green CI run, a satisfied deploy log).
No code path lets an agent assert verified directly.
flowchart TD
A["File task\nPOST /tasks"] --> B{"Admission gate\n(why + acceptance + owner)"}
B -->|fails| A1["400 ErrAdmission"]
B -->|passes| C[("task_items\nlifecycle=backlog")]
C --> D["Promote to ready\nPATCH /tasks/{id}/lifecycle"]
D --> E[("task_items\nlifecycle=ready")]
C -.->|"deps verify"| E
E --> F["Dispatch selects owner\ndispatchHub"]
F --> G["Agent claims task\nSetLifecycleBlocked"]
G --> H[("task_items\nlifecycle=in_progress")]
H --> I{"ClaimLeaseSweeper.Sweep\nstale + no evidence + no live run?"}
I -->|yes, release| E
I -->|no| J["PR opened, closes ARGUS-N"]
E --> BL{"blocked?\nneeds depends_on or blocked_on"}
BL -->|yes| BLK[("task_items\nlifecycle=blocked")]
BLK -.->|"unblocked"| E
H --> CAN[("task_items\nlifecycle=cancelled")]
J --> K["Landing queue lands PR\n(see Landing queue page)"]
K --> L["AttachPREvidence\nsatisfied=true"]
L --> M["VerifySweep -> verifyOne"]
C --> APP{"Verify == approval?"}
APP -->|yes| PEND["task_evidence(kind=approval)\nseeded unsatisfied"]
PEND --> HUM["Items.Approve\nhuman action"]
HUM --> M
M --> N{"AND(all evidence satisfied)\nAND bound anchor exists?"}
N -->|no| H
N -->|yes| O[("task_items\nverified=true\nlifecycle=done")]
O --> P["rollupVerify\nparent epic/feature verified"]
Node annotations¶
File task¶
internal/hub/tasks_api.go:handleCreateTask (L98) -> internal/hub/items.go:Items.CreateItem
(L388). POST /tasks requires why, acceptance, and owner; the project
must already be registered. Inserts task_items with lifecycle='backlog',
verified=false, plus a created row in task_events
(migrations/0027_task_items.sql).
Admission gate¶
Same call, inline validation before the insert. Rejects with ErrAdmission
(HTTP 400) rather than writing a half-formed row.
Promote to ready¶
internal/hub/tasks_api.go:handleSetLifecycle (L372) ->
internal/hub/items.go:Items.SetLifecycleBlocked (L580). Also reachable
automatically: internal/hub/dispatch.go:Dispatch.pollOnce (L529) calls
board.PromoteReadyDependents before every dispatch pass, moving a backlog
task to ready once every task it depends_on has verified (ARGUS-393) --
the dashed edge above.
Dispatch selects owner¶
internal/hub/dispatch.go:Dispatch.dispatchHub (L560). Reads
ReadyTaskCountByOwner/NextTaskFor for the target owner and enqueues an
agent_runs row; it does not itself change lifecycle. Full detail on the
dispatch, claim-lease and heartbeats page.
Agent claims task¶
The dispatched hub-build skill run calls the same
SetLifecycleBlocked endpoint to flip ready -> in_progress once it
starts real work, setting owner to the *-agent identity.
internal/hub/autolink.go:nudgeInProgress (L84) also auto-nudges a task to
in_progress when an open closing PR is detected referencing it.
ClaimLeaseSweeper¶
internal/hub/claim_lease.go:ClaimLeaseSweeper.Sweep (L62). Finds
in_progress tasks owned by a *-agent, unchanged past the lease window
(hub_settings.claim_lease_window, default 20m), with zero evidence and no
live linked agent_runs row, and releases them back to ready
(claim_lease.go:release, L197), logging a claim_released task_events
row and a claim-lease alert.
blocked¶
Items.SetLifecycleBlocked (items.go:580) rejects to=="blocked" unless a
depends_on/blocks task_links row exists or a blocked_on reason string
is supplied (ARGUS-354).
cancelled¶
Same endpoint, no extra gate. internal/hub/verify_rollup.go:rollupState
(L36) treats a cancelled subtree as vacuously complete so it never blocks a
parent's AND-rollup.
Landing queue lands PR¶
See the landing queue page for the full
rebase/CI/merge/renumber sequence. On landed, the WithLandedHook
callback (cmd/hub/main.go:264) runs in order: attach evidence, auto-link
catch-up, verify sweep.
AttachPREvidence¶
internal/hub/landing_evidence.go:AttachPREvidence (L23). Inserts an
already-satisfied task_evidence(kind='pr') row -- merge + green CI are
already proven by the landing queue itself, so this is a fact, not a guess.
A second, independent path (internal/hub/autolink.go:AutoLinkPRs, L55, plus
the POST /tasks/verify handler) attaches pr evidence for any task whose
PR carries a Closes ARGUS-N line, for PRs merged outside the queue.
VerifySweep -> verifyOne¶
internal/hub/verify.go:Items.verifyOne (L191). Re-evaluates every evidence
row from source of truth on every sweep: pr -> PRStatus (merged +
referenced + green CI, or grandfathered pre-ARGUS-452 evidence), ci_run ->
CheckRun, artifact -> file exists under root; approval/deploy_log are
human/ops-set and read-only here.
AND(all evidence satisfied) AND bound anchor¶
verified = len(evidence) > 0 && AND(satisfied) && hasBoundAnchor --
a bare ci_run or artifact row can never alone confer verified
(verify_cirun_binding_test.go, verify_artifact_binding_test.go; the
self-verification hole closed by ARGUS-232/451). Only pr, approval, and
deploy_log are binding anchors.
task_items verified=true, lifecycle=done¶
internal/hub/verify.go:doneStampFor (L176), same transaction as the sweep.
If verified flips true while lifecycle is backlog/ready/in_progress, it is
stamped lifecycle='done', remembering the prior state in
lifecycle_before_done (migrations/0120_lifecycle_done_stamp.sql) so an
un-verify can restore it. blocked/cancelled are left untouched.
rollupVerify¶
internal/hub/verify_rollup.go:Items.rollupVerify (L68), run at the end of
every sweep. An epic/feature with decomposed_at set and every live child
verified gets verified=true, verified_by='checker:rollup' -- a verdict
only the rollup itself can reverse.
Human approval path (approval evidence)¶
Items.Approve (items.go:1117, human-only) satisfies a pending approval
evidence row and re-derives verified inline. Items.Reject marks it
rejected and never verifies. There is also a purely advisory,
non-blocking LLM pass, RunAcceptanceFidelityReview
(internal/hub/acceptance_fidelity.go:80), wired into the same landed hook;
it writes a task_acceptance_reviews row and an event but never touches
task_items.verified (acceptance_fidelity_spec_test.go pins this).
Key tables and endpoints¶
- Tables:
task_items(lifecycle/verified/verified_by/lifecycle_before_done),task_evidence(satisfied/superseded),task_events(append-only audit),task_acceptance_reviews. - Endpoints:
POST /tasks,POST /tasks/{id}/lifecycle,POST /tasks/{id}/evidence,DELETE /tasks/{id}/evidence/{id},POST /tasks/verify(manual sweep trigger),POST /projects/{slug}/dispatch.
Related pages¶
- Dispatch, claim-lease and heartbeats -- what happens between "ready" and the agent actually claiming the task.
- Landing queue -- how the PR referenced above actually gets merged.