| name | xcheck-triage |
| description | Run xcheck triage as a dialogue - present reported findings and record the human's accept/reject/defer decisions in the ledger. Use when the user invokes $xcheck-triage or /xcheck-triage, or says "triage" or "let's go through the findings". The agent is the pen; the human is the decider. |
xcheck launcher — Triage (dialogue mode)
You are the pen, not the decider (XCHECK.md §3 Triage, Agent-as-pen rule). The human makes every decision; you present, record, and never fill gaps with your own judgment.
- Preflight.
audit/XCHECK.md and audit/LEDGER.md must exist. Missing → stop, point to xcheck bootstrap.md.
- Read
audit/XCHECK.md §2–§3 (triage write surface, agent-as-pen) and audit/LEDGER.md.
- Collect: all
reported rows — this is the triage queue, and the ONLY group triage transitions. Also note, for optional review (NOT triage rows): ⚠ needs-human rows, the deferred backlog, unresolved disputed rows.
Legal transitions by source status (§5) — never cross groups: a reported row → accepted/rejected/deferred only. That triad is the ENTIRE triage write vocabulary — XCHECK.md §3 (Triage, Forbidden) bars this session from setting any other status, withdrawn included. A disputed row is NOT resolved here: its resolution to accepted/withdrawn is owned by the Auditor or the human in a dispute round (§5; §9 rule 3), so route it to an Auditor dispute-round session (xcheck-audit disputed) — that session runs the one objection round AND records the human's accepted/withdrawn ruling; triage writes nothing for it now. deferred rows and findings stopped at reopen_limit also have NO triage transition — route them per §9, never assign a new status here. A batch statement ("all critical accepted") applies only to reported rows in that group, never to a disputed/deferred/reopened one.
- Present compactly in the human's language, grouped by severity then dimension: id, title, unit, and a one-line gist of the evidence (open finding files to quote — reading is allowed; you WRITE only the ledger). CF rows get their own presentation: pattern, census size, strategy rung, member ids.
- Collect decisions conversationally. Batch statements apply exactly as stated ("all critical accepted" = accepted for those rows and nothing else). Ambiguous statement → ask; unstated → row stays
reported. You may give an opinion when asked, clearly labeled as opinion; the recorded status is only what the human states.
- Write to
audit/LEDGER.md only, and only the status column of affected reported rows, and only the values accepted/rejected/deferred (§3 Triage write surface: status column only, and no status outside that triad — never withdrawn). A rejected CF also reverts every finding in its members: list to accepted (the one non-reported write triage owns, §8 rule 4). Do NOT write the next column or any other column — the next index is derived later by agent/orchestrator sessions, not by triage. Never touch finding files — agent sessions sync frontmatter later per §2 rule 3.
- Close with a written summary: rows changed (id → status), rows left undecided, and the suggested next launcher (e.g.
xcheck-remediate when findings were accepted).
Lock discipline
audit/.lock serializes writing sessions (§4 rule 8) and is shared with the orchestrator (bin/xcheck), which acquires it atomically with open(O_CREAT|O_EXCL). Match that — never check-then-create (the gap between an existence check and a separate create lets two sessions both win):
- Acquire atomically: create
audit/.lock in one exclusive step that FAILS if the file already exists — e.g. (set -C; printf '%s' '{"pid": <pid or 0>, "role": "<Role>", "started": "<ISO>", "host": "<host>"}' > audit/.lock), where set -C (noclobber) makes the redirect fail atomically when the file exists. For pid, record a process id ONLY if it stays alive for your whole session (e.g. the orchestrator's own pid); a transient shell $$ dies the instant the acquire command returns — while your session keeps running — which would make your own live lock look stale and let another session steal it, so never record $$. An agent-CLI session has no session-long pid: record pid: 0. bin/xcheck reads pid: 0 as a live manual session (os.kill(0, 0) never reports it dead), so the lock stands until your owner-checked release removes it, or — if the session died — a human clears it with xcheck unlock --force. If acquisition fails, another writing session holds the lock — do not start; report the conflict to the human.
- Owner-checked release: hold the lock for the whole session; before deleting, re-read
audit/.lock and confirm its pid+started still match the lock you wrote — delete only then, and on every exit path including early stop. Never delete a lock you do not own.
- Stale lock: a lock carrying a real, dead
pid is stale — clear it with xcheck unlock, never silently steal it. A pid: 0 manual-session lock never reads as pid-dead, so xcheck unlock alone refuses it; clear it only with xcheck unlock --force, and only after the human confirms no writing session is active (§4 rule 8).