| name | xcheck-plan |
| description | Launch an xcheck Planner session - inventory the project and produce audit/AUDIT.md. Use when the user invokes $xcheck-plan or /xcheck-plan, says "plan the audit" or "fill AUDIT.md", or wants to start an xcheck audit in a project that has audit/XCHECK.md installed. |
xcheck launcher — Planner
Thin launcher: preflight + charter routing, delegating the rules to audit/XCHECK.md as the single normative source. Not pure delegation, though, and not zero-normative: for safety and ergonomics some of the methodology's rules are duplicated inside this launcher. Because of that, re-run install-launchers.sh after any change to the methodology (README §7), so the installed copies and generated OpenCode commands never fall behind.
- Preflight.
audit/XCHECK.md must exist in the current project. Missing → stop and tell the user to install xcheck first (xcheck repo bootstrap.md / install.sh).
- Read
audit/XCHECK.md fully. Your role: Planner (§3 role card governs you — mission, reads, writes, stop conditions, forbidden actions).
- Charter auto-pick: planning is done only if
audit/AUDIT.md is a complete plan per the Planner stop condition (§3): at least one dimension backed by a norm source, a unit map, AND a non-empty pass queue — then report that and stop (suggest the xcheck-status launcher). A partial or malformed AUDIT.md (e.g. a pass queue but no norm-backed dimension or no unit map) is NOT done: continue planning to fill the missing parts, or stop with an exact list of what is missing. Otherwise your charter is: inventory this project and its norms; fill audit/AUDIT.md (dimensions with norm sources, unit map, pass queue, limits). A user-supplied argument overrides this charter.
- Announce the charter in one line, then execute the role exactly per XCHECK.md.
- Session hygiene (F-0120): create every temporary artifact (scratch copies, harnesses, fixtures, mutation copies, marker files) OUTSIDE the project tree — in a system temp dir — never inside the project. The orchestrator's courier ships the project tree, so a stray in-tree file is committed as material; clean up before you exit.
- Converse with the human in their language; artifact content follows the XCHECK.md language rule.
Construal (§4 rule 9)
Off by default (§10 construal_gate). When the orchestrator dispatches you with a charter that begins "Before any effect on the material, write your OPERATIONAL CONSTRUAL", that charter IS the gate: copy audit/templates/construal.md to audit/construals/<key>.md — the key is XCHECK_CONSTRUAL_KEY, read it from the Orchestration context line appended to your role-prompt, since your shell env may not expose it — fill all five sections with real content, set status: proposed and session: to your own XCHECK_SESSION_ID, then STOP. Touch nothing else: writing the construal IS the whole session.
Never set status: admitted and never fill admitted-by:. A producer that admits its own construal has licensed its own misreading; both xcheck lint and the gate refuse it by name. Admission is a separate act by a different party — a human, or a construal_envelope declared in AUDIT.md with a live recheck-by date. Your construal is EVIDENCE the admitter inspects, never authority you grant yourself.
State your own reading, not a paraphrase of the charter. If your task frame and the charter's words diverge, that divergence is the point of the file and the reason the gate exists.
Refusal (§5)
If you accept a charter and cannot execute it, record a refusal — do not halt silently and do not quietly substitute a narrower task. Set refusal: in the finding's frontmatter to exactly one reason from the closed vocabulary — out-of-competence, blocked-dependency, charter-ambiguous, norm-conflict, material-missing, cost-exceeded — and write what is actually missing, and what would unblock it, into that finding's ## Refusal section. Both halves are required: xcheck lint rejects a bare reason code, because a category name carries no obstacle forward.
Do NOT change the finding's status. A refusal is about this attempt, not about the finding: the charter stays in force, the work is still owed, and the next session inherits both the work and your reasons. A refusal is not a dispute (which contests the finding itself) and not a rejection (which is Triage's call, and Triage is the human).
Lock discipline
audit/.lock serializes writing sessions (§4 rule 8) and is shared with the orchestrator (bin/xcheck). It is a DIRECTORY, acquired atomically with mkdir — the second writer's mkdir fails with EEXIST, so the create IS the acquisition. Match that — never check-then-create (the gap between an existence check and a separate create lets two sessions both win).
-
Orchestrated child — skip acquisition (F-0093): the orchestrator (bin/xcheck) signals inherited lock ownership through TWO channels, because process env does not reach a sandboxed command runner on every agent platform (a Codex-style runner runs your shell where env shows nothing of the launched CLI's variables): XCHECK_LOCK_INHERITED=<nonce> in your environment AND an Orchestration context line appended to your role-prompt carrying the same XCHECK_LOCK_INHERITED=<nonce>. Read the nonce from whichever channel you can see — the prompt line always reaches you. If that nonce is present AND equals the nonce in audit/.lock/owner, the orchestrator launched you and is already holding the writing lock around this whole transaction — do NOT mkdir audit/.lock (it would fail EEXIST on your own parent's lock and abort you), do NOT write an owner record, and do NOT remove the lock on exit; the orchestrator owns its release. If the nonce is present but does not match the on-disk owner (or the owner record is missing), treat it as a foreign lock — stop and report the conflict to the human. If neither channel carries a nonce you are a standalone session — acquire atomically as in step 1 below.
-
Acquire atomically: create the lock DIRECTORY in one step that FAILS if it already exists — mkdir audit/.lock — then write the owner record inside it, including a fresh per-session nonce — a unique random token you generate at acquire time (16 hex chars from a random source, matching bin/xcheck, which writes os.urandom(8).hex()): printf '%s' '{"pid": <pid or 0>, "role": "<Role>", "started": "<ISO>", "host": "<host>", "nonce": "<nonce>"}' > audit/.lock/owner. mkdir is the atomic gate (a second mkdir on an existing directory fails); the nonce — NOT the pid+started pair, which is not a unique owner id (two agent-CLI sessions both record pid: 0 and can share the same ISO second, giving a byte-identical record) — is what identifies you for release. 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 (the nonce, not the pid, is your identity; the pid only drives liveness). 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 mkdir 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 removing, re-read audit/.lock/owner and confirm its nonce still matches the one you wrote at acquire — check the nonce, never the pid+started pair (a pid: 0 manual session can collide on it), exactly as bin/xcheck's release() does — only then rm audit/.lock/owner && rmdir audit/.lock, on every exit path including early stop. Removing your own record first and then rmdir means a foreign owner's record keeps the directory non-empty, so rmdir can never remove a lock you do not own. Never delete a lock you do not own.
-
Stale lock: a lock carrying a real, dead pid is stale, but non-force xcheck unlock no longer removes it — clearing a lock by pathname cannot be made race-free against a concurrent clear + re-acquire (F-0095), so plain xcheck unlock only diagnoses staleness and never deletes. Clear any stale lock — a dead-pid lock, a pid: 0 manual-session lock (which never reads as pid-dead), or a pre-directory .lock FILE (legacy, not auto-migrated) — with xcheck unlock --force, and only after the human confirms no writing session is active (§4 rule 8). Never silently steal a lock you do not own.