Skip to content

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.