ZAgents Docs

Checker agents

Independent verification agents that gate issue completion until acceptance conditions hold.

Private fork only — this feature exists in ZurabDev/multica and is not available upstream. See PRIVATE_CHANGELOG.md for the change log.

What is a checker?

A checker is a verification agent attached to an issue. Whenever the working agent replies on the issue, the platform fans the comment out to every active checker that whitelists the current issue status (or to all of them if no whitelist is set). Each checker inspects the work and posts a verdict in a comment. The issue auto-promotes to done only when every checker approves. If a checker exhausts its retry budget, the issue stays in its current status with a system comment mentioning the owner.

Round 4 (May 2026): checker re-runs are now status-independent. The legacy "agent moves to in_review" trigger still works as a backstop, but the primary signal is every new comment from the issue's assignee-agent. This closes the stuck-pipeline class of bugs that produced ticket 20294e12. The per-definition trigger_statuses whitelist scopes when this fires (NULL/empty = any status).

The mental model is Claude Code's /goal evaluator: a second, independent "mind" that decides whether the condition you wrote in markdown is satisfied. The main agent does the work; the checker decides if it counts.

Anatomy

  • Definition — reusable, workspace-level template: a name, the agent that runs it, the markdown acceptance condition, and a default max_retries.
  • Assignment — attaches a definition to one concrete issue. Carries its own priority_group, override max_retries, and runtime state (retry_count, status).
  • Run — one verdict attempt. Has an immutable retry_index, a verdict once finished (approve / reject / timeout / error), and (for rejects) a reference to the comment that contained the verdict.

Multiple checkers on one issue are allowed:

  • Same priority_group → parallel. All checkers in group 0 run together; group 1 starts only after group 0 fully approves.
  • Different priority_group → sequential. Cleanest way to chain "lint then security then UX review".

Lifecycle

working agent: …does the work…
working agent: posts a reply
listener:      MZHandleCommentCreatedForChecker (round 4 — primary trigger)
  – author == issue.assignee (main agent)? yes
  – author NOT a checker on this issue? yes
  – issue.status passes trigger_statuses whitelist? yes
  – no pending checker task already in flight for the pair? yes
  → MZCheckerEnqueueCurrentGroup(issue, statusFilter=current)
checker A:     reads issue + comments + condition_md
checker A:     comment "MZ-CHECK: APPROVE: looks good"
service:       checker A → approved
checker B:     comment "MZ-CHECK: REJECT: missing test for edge case @agent:42"
service:       checker B run finalized, retry_count++; B stays active
working agent: reads REJECT mention, fixes, posts the next reply
listener:      enqueues B again (retry_index=1)
checker B:     comment "MZ-CHECK: APPROVE"
service:       all of group 0 approved → next group OR issue → done

Round-4 detail: no status change is needed between cycles. The MZHandleIssueUpdatedForChecker legacy listener still fires on status_changed → in_review as a backstop (e.g. for a human-initiated status flip), but the comment listener is the primary progress signal now.

If group 0 advances to group 1, it auto-enqueues. If group N is the last and all approved, the platform sets issue.status = done. If any checker hits retry_count == max_retries with no approve, it flips to exhausted and the platform posts a system comment with @member:<owner-id>; the issue stays in its current status.

Verdict format

The checker writes one comment per run. The first non-blank line must be exactly:

MZ-CHECK: APPROVE

or

MZ-CHECK: REJECT: <one-line reason>

Trailing body is free-form markdown — concrete feedback, references, and on REJECT a @agent:<uuid> mention of the agent that did the work so they see the rejection. The platform parses only the first line; the rest is for humans and the working agent. MZ-CHECK: EXHAUSTED and unknown prefixes are not verdicts — they're reserved for the platform.

Managing checkers

From the UI

  • Issue sidebar → Checkers — attach a definition, pause/resume, change max retries or priority group, force a Run now, view full run history.
  • Workspace settings → Checkers — manage workspace-level definitions and the default max_retries for newly created definitions.

Definition form (Settings + issue sidebar inline)

When you create or edit a definition, you no longer paste an agent UUID. Pick an agent from the dropdown — workspace agents are searchable and avatars/names are shown. If a definition references an agent that has since been archived, the form surfaces an explicit warning so you re-select another active agent before saving.

Working with definitions from the issue sidebar

The attach dialog now does more than pick existing definitions:

  • + New definition — create a new definition directly from the issue sidebar without leaving the page. After it's created, it auto-selects so you can immediately attach.
  • Pencil icon on any row — edit that definition (name, agent, markdown condition, default max retries) inline. The change persists workspace-wide; your selection in the attach dialog is preserved.
  • Save this max retries as the definition's default (currently: N) — checkbox that appears below the override input when your max_retries differs from the definition's default. OFF by default. Tick it to push the value back to the definition's default for future issues; leave it OFF to keep the override per-assignment only. The dialog updates the definition first, then attaches; if the definition update fails, the attach is cancelled.
  • Manage definitions… — opens the same management UI that lives in Workspace Settings (policy + list + create/edit/archive) as a modal on top of the attach dialog. Use this when you need a heavier clean-up (archive stale definitions, change the workspace policy) without losing the work-in-progress attach state. Archiving the definition you had selected in the attach dialog clears your selection automatically.

From the CLI

Reusable definitions:

zagents checker-def list
zagents checker-def create   --name "Lint check"  --agent <id|name>  --condition-file ./lint.md  --max-retries 5
zagents checker-def update   <defId>              --name ... --condition-file ... --max-retries ...
zagents checker-def archive  <defId>

Per-issue assignments:

zagents issue mz-check list     <issue>
zagents issue mz-check attach   <issue>  --definition <defId> [--group 0] [--max-retries 5]
zagents issue mz-check detach   <issue>  <assignmentId>
zagents issue mz-check pause    <issue>  <assignmentId>
zagents issue mz-check resume   <issue>  <assignmentId>
zagents issue mz-check update   <issue>  <assignmentId> [--max-retries N] [--group N]
zagents issue mz-check run-now  <issue>  <assignmentId>
zagents issue mz-check history  <issue>  <assignmentId>

Create an issue with checkers already attached:

zagents issue create  --title "..." --description-file ./desc.md \
                      --mz-checker <defId>[:<group>[:<maxRetries>]] \
                      --mz-checker <defId2>:1:5

The --mz-checker flag is repeatable. The colon-separated form is <defId>[:<group>[:<maxRetries>]] — both sub-segments are optional; 0 for maxRetries means "inherit definition default".

Architecture diagrams

Two tables you'll meet most often: mz_checker_definition (reusable template) and mz_checker_assignment (template attached to an issue). Runs are an audit trail; the workspace settings table holds the default retry limit.

Schema

erDiagram
    workspace ||--o{ mz_workspace_checker_settings : "policy"
    workspace ||--o{ mz_checker_definition : "templates"
    agent ||--o{ mz_checker_definition : "executor"
    mz_checker_definition ||--o{ mz_checker_assignment : "attached to issues"
    issue ||--o{ mz_checker_assignment : "verified by"
    mz_checker_assignment ||--o{ mz_checker_run : "audit trail"

    mz_workspace_checker_settings {
        UUID workspace_id PK
        INT default_max_retries
    }
    mz_checker_definition {
        UUID id PK
        TEXT name
        TEXT description_md
        UUID agent_id FK
        INT default_max_retries
        TIMESTAMPTZ archived_at
    }
    mz_checker_assignment {
        UUID id PK
        UUID issue_id FK
        UUID definition_id FK
        INT priority_group
        INT max_retries
        INT retry_count
        TEXT status
    }
    mz_checker_run {
        UUID id PK
        UUID assignment_id FK
        INT retry_index
        TEXT verdict
        TEXT reason
    }

Assignment state machine

stateDiagram-v2
    [*] --> active : Attach
    active --> paused : admin pause
    paused --> active : admin resume
    active --> approved : MZ-CHECK: APPROVE
    active --> exhausted : REJECT && retry_count==max_retries
    active --> active : REJECT && retry_count<max_retries
    approved --> [*] : terminal
    exhausted --> [*] : terminal (manual review)

End-to-end verification flow

When the main agent moves an issue to in_review, the platform fans out the current priority group, the checker comments its verdict, and the server advances or stalls the issue accordingly.

sequenceDiagram
    autonumber
    participant Agent as Main agent
    participant API as Server
    participant Checker as Checker agent
    participant UI

    Agent->>API: status=in_review
    API->>API: enqueue current priority group
    API-->>Checker: spawns agent_task_queue (context.kind=checker)
    Checker->>API: comment "MZ-CHECK: APPROVE" or "MZ-CHECK: REJECT: <reason>"
    API->>API: parse verdict, finalize run

    alt APPROVE
        API->>API: assignment → approved
        alt all groups cleared, none exhausted
            API->>API: issue → done
        else next group exists
            API->>API: enqueue next group
        end
    else REJECT, retries left
        Note over Checker,UI: Checker's comment @mentions the executor;\nagent fixes & flips to in_review again
    else REJECT, no retries left
        API->>API: assignment → exhausted, post system comment
    end

Priority groups

Same priority_group runs in parallel; the next group starts only after the previous one fully approves. Any exhausted blocks auto-promote to done.

Group 0:  Checker A   Checker B   Checker C   ← parallel
            ↓ all approved
Group 1:  Checker D                            ← runs next
            ↓ approved
Group 2:  Checker E                            ← last
            ↓ approved
issue.status = done

Need the full architecture (component diagram, schemas with all constraints, "Save as default" sequence, full lifecycle of one verification)? See MZ_CHECKER_ARCHITECTURE.md at repo root — the canonical fork-only reference.

When does a checker stop?

  • Approved — the checker said APPROVE. Terminal.
  • Exhausted — retry_count == max_retries and the latest verdict is REJECT (or never approved). Terminal until a human edits the assignment or detaches it.
  • Paused — admin action. The checker won't be enqueued on in_review transitions; resume to re-enable.
  • Detached — admin removed the assignment. The runs history is dropped with it.

A workspace-level setting (Settings → Checkers → Checker policy → default max retries) sets the fallback max_retries for newly created definitions. Per-definition and per-assignment overrides win over this default.

When to attach a checker

  • Acceptance criteria you can express in markdown. "All tests green", "no console.log in shipped code", "the migration includes both up and down files". A checker that gets a vague condition will guess.
  • Cross-checking dimensions the working agent isn't watching. Lint, types, security, UX, copy review — each can be a separate checker so an APPROVE means every dimension passed.
  • You want the issue blocked until verification. The whole point: done only happens after every checker says yes. If you'd rather have a soft signal, use a comment instead.

If your "checker" is really a follow-up task — implement it as a regular issue with a blocking dependency (zagents issue mz-block). Checkers are read-only verifiers, not workers.

What if a checker doesn't run?

Round 4 makes the comment listener the primary trigger, so the legacy "agent never set in_review" failure mode is gone. The remaining causes — in order of frequency:

  1. Daemon binary is older than MZMinDaemonVersion. The multica CLI compiles the prompt templates into the binary. Older daemons don't have the round-4 "NEVER set done directly" copy and may move issues straight to done, bypassing checkers. The server logs WARN ... daemon binary out of date for checker prompts on every claim. Fix:

    zagents update
    zagents daemon restart
  2. The trigger_statuses whitelist excludes the current issue status. If the definition has trigger_statuses = ['in_review'] and the issue is in in_progress, automatic re-runs are intentionally suppressed. Either widen the whitelist or click "Run group" in the issue sidebar — manual triggers ignore trigger_statuses by design.

  3. A live checker task is already in flight for the (issue, agent) pair. The listener checks MZHasPendingCheckerTaskForPair to avoid spamming. If a previous run is still queued/dispatched/running, comments coalesce into the existing task. Wait for the task to terminate, or detach + re-attach the checker to forcibly cancel it.

  4. The author of the comment is itself a checker on this issue. Self-loop defense: a checker's own MZ-CHECK comment (and any follow-up commentary) never triggers another checker run. If you intended to comment on behalf of the main agent, post from the main-agent identity.

If none of the above applies and you still don't see runs, the server logs (backend-error.log) trace every mz: checker enqueue attempt and any failure reason.

Forcing a run from the UI / CLI

When automatic triggers don't fit your scenario:

  • UI — issue sidebar → Checkers → "Run group" button (visible only when at least one assignment is active). Enqueues every active checker in the current priority group, ignoring trigger_statuses.

  • Per-row UI — RefreshCw icon on a single row enqueues just that assignment.

  • CLI —

    zagents issue mz-check run-group <issue>      # current group, ignores trigger_statuses
    zagents issue mz-check run-now <issue> <asg>  # single assignment, current group only

See also

  • zagents issue mz-block — agent-blocking dependencies between issues. Different mechanism, often used alongside checkers.
  • PRIVATE_CHANGELOG.md (repo root) — full implementation history, file layout, upstream-sync risk register.
  • Goal evaluator in Claude Code: code.claude.com/docs/en/goal.md — the conceptual ancestor of this design.