LazyZCode Documentation

English · 简体中文

The complete guide to the discipline layer for ZCode: plan → execute → take evidence → never stop half-done.

   
Install Installation · Recommended environment
Getting started Overview · Your first goal loop · FAQ
Triggers & protocol Trigger words · The zw protocol · Goal loop commands
Concepts The plan gate · Evidence discipline · The continuation budget · Discipline agents · Hooks & lifecycle · Rate-limit discipline
Reference CLI reference · Diagnostics · State & configuration · Compatibility & limitations

Installation

One package installs both halves of LazyZCode: the lazyzcode:zw plugin (skills, hooks, agents) and the lzy CLI (the goal-loop state machine).

Prerequisites

Install

npm i -g lazyzcode
lzy install

lzy install does three things: deploys the plugin payload to the engine’s plugin cache, writes LazyZCode’s own registry entry, and enables the plugin through the engine’s official plugins enable command. It never writes your config.json — enabling flows only through the engine’s own CLI (ADR-0001).

Verify it worked

lzy status   # quick check; exit code 0 = no fail-level findings
lzy doctor   # deep local diagnostics (zero telemetry)

Both commands accept --json: a machine-readable contract output (schemaVersion starts at 1 — the version discipline is additive-only: new fields are optional, absent fields are never emitted as null, and consumers must ignore unknown fields; the shape is pinned by a contract test) for scripts and external observers — no text scraping required.

Inside a fresh ZCode session, type zw at the start of a prompt — the orchestration bootstrap should arrive, and the model’s first line should read **ZW** engaged — LIGHT tier (or HEAVY).

Uninstall

lzy uninstall

Removes the deployed cache and the registry entry, preferring the engine’s official plugins uninstall path.

Item Recommendation Notes
Operating system macOS, Windows, or Linux Engine layout detection covers all three (ADR-0011).
ZCode app Desktop, logged in Hooks and the plugin load through the desktop engine.
Node.js ≥ 24, via nvm or Homebrew The hook-node doctor check reports exactly which resolution path your hooks will use.
Project A git repository Evidence binding requires commits to exist; F-item evidence is captured after a commit.
Launch style Either Launching ZCode from the terminal or the Dock both work; the Dock case is handled by the run-hook launcher.

Overview

LazyZCode installs four things:

  1. One plugin (lazyzcode:zw) whose skill text carries the full orchestration protocol — triage, tier selection, planning, execution, and evidence rules.
  2. Six hooks wired to the engine’s lifecycle: SessionStart (re-inject loop state), UserPromptSubmit (trigger words), PreToolUse (command-layer H3R gate — dormant prototype), PostToolUse (comment advisory), PostToolUseFailure (same-tool failure tripwire), Stop (bounded continuation).
  3. Three read-only agents: lazyzcode:explorer, lazyzcode:plan-reviewer, lazyzcode:qa-executor.
  4. One CLI (lzy) implementing the goal loop as a state machine that survives session restarts: register → plan gate → execute → evidence → finish gate.

The mental model: a plan checkbox is a claim; only real-surface evidence is a fact. LazyZCode’s job is to make the agent convert claims into facts — and to pull it back when it tries to stop before that conversion is complete.

Your first goal loop

Open a fresh ZCode session in your project and type:

zw implement a CSV export endpoint with tests

What happens:

  1. The UserPromptSubmit hook matches the sentence-initial zw and injects the orchestration bootstrap.
  2. The model announces **ZW** engaged — <tier> tier and triages the task (see The zw protocol).
  3. It registers a goal with the CLI (lzy loop register …) and writes a plan file into .lazyzcode/plans/.
  4. The plan is adopted through the plan gate — HEAVY plans must first pass the lazyzcode:plan-reviewer agent.
  5. The model executes step by step, calling lzy step done <ID> as steps complete. F items require --evidence bound to the current tree hash.
  6. lzy loop finish runs the final gate: all steps done, all evidence fresh. Only then is the goal complete.

If the model stops while the loop is open, the Stop hook pulls it back — at most 2 continuations per session. To resume after an interruption (including a rate-limit kill), just send zw continue (zw 继续); the loop state in .lazyzcode/ is untouched.

To free the slot after a finished loop, run lzy loop reset.

FAQ

Where does LazyZCode keep its state? In .lazyzcode/ inside your project (loop state, plans, archived evidence). It is deliberately separate from ZCode’s own .zcode/. Add it to your .gitignore if you do not want to commit it.

Does it phone home? No. Zero telemetry. Everything lzy doctor reports is computed locally from your own files and engine logs.

Does it write my config.json? Never. Enabling goes through the engine’s official plugins enable; the installer only deploys files and writes LazyZCode’s own registry.

Why didn’t my zw trigger? Matching is stratified: a bare zw fires only at the start of the prompt. Mid-sentence zw is ignored (that is intentional — “check the zw hooks” is a question, not an invocation). Use the explicit lazyzcode:zw anywhere, or ulw/ultrawork anywhere word-bounded.

Why did lzy loop finish reject my evidence? Evidence is bound to the tree hash of the commit that was current when it was captured. If you changed code after capturing, the evidence is stale by construction — commit, then re-capture with lzy step done <ID> --evidence ….

I hit [1302] rate limit. Is LazyZCode broken? No — that is the GLM plan’s account-level concurrency limit. lzy doctor’s rate-limit line reports your measured pressure and a suggested session budget. See Rate-limit discipline.

My hooks do nothing at all. Your engine was probably launched without node on its PATH (common when ZCode.app starts from the Dock). The bundled run-hook launcher resolves node automatically — nvm/Homebrew on POSIX, nvm-windows/Program Files on Windows; lzy doctor’s hook-node line tells you what it found. Details in docs/diagnostics/2026-09-07-hook-spawn-env.md.

Can I run two goal loops at once? One loop per project directory. A finished loop still occupies the slot until lzy loop reset clears it.

Can I use the CLI without the plugin (or vice versa)? Yes. The CLI works anywhere; the plugin needs the engine. The workflow works best with both — the hooks and skill text drive the discipline that the CLI enforces.

Trigger words

Trigger Fires Example
zw Start of the prompt only (leading whitespace allowed) zw fix the flaky test
lazyzcode:zw Anywhere; full-width colon : accepted explain what lazyzcode:zw does*
ulw / ultrawork Anywhere, word-bounded ultrawork this refactor
/zw or the Skill tool Explicit invocation —

* that example does not engage the loop — it is a mention. Injection is conditional: an invocation engages the protocol; a mere mention is ignored and answered directly (“Mention ≠ invocation”). zwift, azw, and mid-sentence zw never fire.

The zw protocol

The injected skill text (plugin/skills/zw/SKILL.md) defines the contract the model follows:

  1. Open with exactly **ZW** engaged — <LIGHT|HEAVY> tier.
  2. Triage the tier. LIGHT for small, contained, low-risk work (a 1–2 item plan, one F item). HEAVY for multi-file features, architecture, or anything risky or vague — explore first, plan every step. Escalating LIGHT → HEAVY is always allowed — and persisted: register with --tier heavy or run lzy loop tier heavy before adopting (the HEAVY review gate is enforced by the machine off the persisted tier). Downgrading is never allowed.
  3. Register the goal with the CLI.
  4. Plan with an N/F checklist (see The plan gate).
  5. Execute step by step, committing work and calling lzy step done <ID> with notes — and evidence for F items.
  6. Capture evidence on real surfaces (see Evidence discipline).
  7. Finish through the CLI gate (see Goal loop commands).

The protocol also carries behavioral red lines (no fake evidence, no silent scope abandonment) and the rate-limit rules below.

Since 0.1.0 the protocol text also annotates enforcement levels — L0 protocol convention, L1 CLI machine gate, L2 trusted host event, L3 external effect — so a reader always knows whether a rule is kept by promise or by machine, and carries a second axis next to tier: risk_class (LOW / MED / HIGH / RESTRICTED, upgrade-only), with HIGH+ barred from unattended lanes and RESTRICTED a hard block. Approvals bind immutable hashes (INV-05): plan adoption binds planHash at L1; a conversational nod stays L0 until the exact-hash human gate lands.

Since 0.1.1 pull-back is claim-gated: only a session that claimed the goal (an invocational trigger like zw 继续) is subject to Stop pull-backs; an empty claim set means nobody is pullable, so bystander sessions are structurally exempt. A session can opt out explicitly with zw standdown — a standdown flag makes the Stop hook release that session read-only until a claiming trigger clears it or the goal is reset. And the host workspace must be a git repository: lzy loop register hard-rejects a non-git host with git init guidance, because evidence binds git trees and a non-git host cannot pass finish (ADR-0019).

Requirement contracts (0.3.0)

By default a goal loop approves an execution plan: the UPS human gate binds your 「批准 <短码>」 to the planHash, and changing the plan mid-flight re-opens the gate. Since 0.3.0 a goal can instead be bound to a **requirement contract** — an immutable markdown file describing what to build (acceptance items with stable ids), where it may write (`scope:`), which delivery endpoint it targets, and which project-recipe version it trusts:

lzy loop register <slug> --title "…" --contract .lazyzcode/contracts/<task>.md

You approve the contract once (「批准 」); afterwards the agent replans inside the boundary without asking again — deleting an acceptance item, stepping outside `scope:`, or a changed `lzy.project.json` are all machine-rejected until you approve a new contract. You can revoke with 「撤回 」: the next gated action refuses, work already done stays honestly recorded. Read the bound contract and its approval ledger with `lzy contract show` / `lzy contract auth` (read-only — approvals and withdrawals exist only as real user messages). Goals registered without a contract keep the classic plan-approval gate.

Controlled execution & receipts (0.3.0 M2)

Check recipes in lzy.project.json can now actually run: lzy verify run <checkId> executes the recipe under the controlled executor (argv array, no shell, timeout kill, env-name whitelist) and records a checksum receipt under .lazyzcode/verify/ (reset-surviving; tamper fails closed). Raw output lands in raw/, kept separate from human summaries. A recipe that declares inputPaths (files or directories) and passes lzy verify qualify — the adversarial qualification that injects a change into every declared input and requires detection plus byte-identical restore — may reuse old receipts via lzy verify reuse --of <runId>; any failed question (non-empty inputs / qualification on record / input snapshot unchanged / manifest & env unchanged) falls back conservatively with named reasons, and reuse only appends an applicability judgment — the base receipt is never rewritten. lzy verify ci queries GitHub check-runs read-only and binds the commit identity: a recorded sha behind the current HEAD is labeled 非现行 (not current), and gh absent/offline reports blocked with recovery hints — never a silent pass.

Bounded queue & cumulative budget (0.3.0 M3)

Multiple authorized items can run continuously across interruptions. lzy queue add registers an approved contract as a queue item (proposed → authorized once the approval is effective → ready when dependencies / project / budget / lease / plan are all satisfied); lzy queue dispatch runs items serially: a dispatch transaction is written in-lock (occupancy registration) → the goal is registered or resumed → drive → finish → per-segment settlement → queue confirmation → slot recycle → next item. After a crash, restarting dispatch reconciles before acting: each unsettled transaction is judged against the current goal (same goal resumes without re-registering; a done goal settles honestly; a missing goal fails the item with a human-pointer), never re-dispatching blindly and never resetting another goal. The cumulative budget (lzy queue budget) binds the contract hash and lives outside the resettable goal: switching tasks, restarts and retries never refresh it, duplicate receipts dedupe by key. Point enforcement uses the approved approximate-limit semantics: usage is queried per segment sessionId; metering absence / unpriced models / unsettled occupancy are never counted as zero — point-limited dispatch stops with an explicit record until a human resumes it via --resume-points; in-flight consumption at SIGKILL is declared as a killed-inflight entry, never flattened to zero.

Limited delivery B/C (0.3.0 M4)

Delivering all the way to the project’s main branch (endpoint B) or a verified live environment (endpoint C) needs its own authorization on top of the requirement contract: lzy delivery request B|C --contract <file> binds a per-endpoint delivery contract and asks for a UPS approval of its hash. The merge action (lzy delivery act B) is machine-gated: dual B∧C authorization (when merging to a branch that triggers a deploy), PR head/base drift re-check, and green required CI on the PR head — then it merges bound to the PR head, reads back the actual merge SHA, and polls CI on that SHA. Pages verification (lzy delivery act C) waits until the Pages build commit matches the merge SHA and checks the live HTTPS content against an expected marker. Every action is preceded by an intent record (target identity, planned argv) in .lazyzcode/delivery/, and lzy delivery readback B|C classifies outcomes after timeouts or disconnects — a done intent is final, so an already-successful action is never re-executed.

Queue x delivery orchestration (0.3.1)

Items can target a delivery endpoint directly: lzy queue add ... --endpoint B --delivery-b <B contract> (endpoint C needs both --delivery-b and --delivery-c; endpoint A takes no delivery contract). After the goal finishes normally, dispatch runs the delivery chain itself: bind contracts, act/read-back B (then C), and only a done read-back marks the item completed (completedEndpoint records the reached endpoint). Delivery approval codes are available at enqueue time: queue add prints each contract’s short code and 「批准 」 is recorded through the same UPS approval channel (the goal does not exist yet before dispatch - the approval attaches to the item); an item missing any declared approval is never ready. For unattended runs the action parameters come from the contract (B: `repo/base/branch/pr-title/pr-body`; C: `repo/expect-marker/content-url` plus multi-page `page:` lines, each checked for 200-and-marker) - a contract missing required keys is rejected at enqueue, not at dispatch time. HEAVY items enter on a pre-enqueue plan-review PASS plus a plan hash (re-checked before dispatch: a changed plan voids the review); high-risk work stays out of the queue (HIGH+ never enters unattended lanes).

Delivery not completed (PR unmerged / build not aligned): the item lands in failed with the recovery route in its blockedReason - repair with lzy delivery readback/act, then lzy queue reconcile acknowledges it as completed without re-running the goal. A dead delivery chain (past the bound or a dead holder process) is settled from the intent ledger by reconcile - never a deadlock; infrastructure aborts (lock contention / corrupt intent ledger) end as a distinct “infrastructure abort” state so the two causes stay tellable apart.

Cross-project verification recipes (pilot notes)

Three field rules from driving other projects to end-to-end acceptance (0.3.0 three-repo pilots): (1) projects whose config layer ignores XDG (e.g. openchamber’s hard-coded os.homedir()/.config/...) need HOME overridden as well for isolated runs, or they will write the user’s real config; (2) pin version lines (e.g. an opencode v2 vs 1.18.x fixture break) - isolated installs in the recipe, never the system’s current version; (3) rebuild before browser evidence on source-tree projects (build:web etc.) - a stale dist yields false red/green.

Migration (0.3.0 M5)

Explicit migration for legacy loop state: lzy migrate preview <root> is a read-only preview (draft contracts with authorization NONE; refuses with an active goal); lzy migrate apply <root> runs backup → staging → validation → atomic switch and stamps the .lazyzcode/state.json version entry (written last = commit point). In-flight goals become drafts in drafts/ (authorization NONE — turning one into a real contract needs a fresh UPS approval; old approvals never escalate); finished goals and legacy evidence are preserved byte-exact. Reruns are idempotent per task identity with a phase journal for crash recovery; lzy migrate status is the read-only face. A live lease/process refuses migration; a corrupt or unknown-version goal.json stops before any write; update/sync never migrate automatically (ADR-0029).

From 0.4.0 the state version stamps 0.4.0; older states stay readable, and apply is phase-journaled end to end — a kill during backup/stage/switch is resumed idempotently by rerunning the same command (backup and act side effects are never replayed).

Policy evaluation (0.4.0 M5)

0.4.0 ships a paired evaluation harness (scripts/evaluation/run-pairs.mjs) that measures a candidate against the published baseline on sealed tasks from real repositories: baseline/candidate arms, three trials per task, a pre-registered interruption+resume leg, and an independent sealed oracle that judges the delivered repo. The 0.4.0 batch report — including the honest quality-gate reading (not met; no evidence of quality benefit) and its threat boundaries — lives at docs/reviews/v040-policy-evaluation.md. Evaluation informs adoption decisions; it is never wired to any automatic gate.

Writing scope declarations that qualify (0.4.0)

lzy review qualify challenges a review-scope declaration against ten axes; these patterns keep a declaration grantable instead of refused:

OS sandbox for independent reviews (available since 0.6.0)

This capability shipped in 0.6.0 and is included in 0.6.1. The guarantee depends on the detected platform and authentication tier described below. When the Seatbelt probe passes and an allowed personal API key is available, lzy review run and recheck use the os-sandbox tier, wrapping the engine and its descendants in macOS Seatbelt. Candidate snapshots and factual inputs are read-only; the isolated HOME and tmp are writable. Path metadata queries remain allowed, so file existence is not hidden. An environment allowlist excludes inherited host secrets, NODE_OPTIONS, DYLD settings and proxies. PERSONAL configuration still requires an explicit provider allowlist; BUILTIN templates are copied into the isolated HOME too. Without both tier prerequisites, including Linux/Windows, OAuth-only setups and no enabled allowed key, reviews use app-layer isolation: isolated HOME and facts-only checks remain, authentication uses envAuth or an OAuth credential copy, and there is no OS confinement. The run record’s isolation {level,backend,auth} and doctor disclose the tier. Unreadable or unrecognized provider configuration still refuses launch.

In the os-sandbox tier, network access goes through a parent-process proxy for endpoints from allowed PERSONAL api.baseUrl values or BUILTIN template defaults. Plain HTTP forwarding checks the exact origin. HTTPS permits only CONNECT to the configured hostname:port; it does not inspect TLS SNI, HTTP Host or paths inside the tunnel. DNS addresses are resolved and pinned when the proxy starts, not resolved again per request in this run. Explicitly allowed local/private API endpoints remain reachable. An unresolved endpoint or unavailable proxy refuses sandbox launch. Other drive/queue defaults are unchanged.

Normal exit, timeout and catchable interruption attempt to reclaim the managed process group and credential copies/tmp. Descendants in independent process groups retain OS restrictions in the sandbox tier, but may survive cleanup; ordinary host shell children can use such groups too. After SIGKILL, use lzy doctor to inspect residues and lzy review cleanup <runId> to remove registered configuration copies and tmp only after their owner is no longer alive. This command does not prove all descendants have stopped. Transcripts and usage databases remain. Allowed API credentials are readable by the reviewer and the allowed endpoint can receive data. This is not a container/VM and does not guarantee prevention of all exfiltration, review correctness or a hard spending cap. See ADR-0038.

Goal loop commands

lzy loop register <slug> --title "…" [--tier heavy]   # planning state; tier defaults to light
lzy loop plan <file> [--review "…"] [--force]
lzy loop start                          # records the base tree hash + prints the measured 并发纪律 advisory
lzy loop subject add <path>             # declare a sibling repo root (executing-only; validated: git repo, no containment)
lzy loop subject remove <path>          # remove a subject (missing-deadlock escape; evidence invalidation semantics apply)
lzy loop subject list                   # list the subject set (empty = host-only)
lzy loop tier heavy                     # tier upgrade, one-way (machine gate is adoption-time)
lzy loop claim [<id>] [--release]        # per-step claim (anonymous, 48h mutex); bare lists claimable steps
lzy step done <ID> [--note "…"] [--evidence "…"] [--evidence-file <file>]…
lzy loop status                         # progress, next step, evidence freshness, tier/subjects/snapshot
lzy loop verify                         # evidence freshness audit (exit 1 = stale/unbound); prints per-tree head/dirty lines
lzy evidence red <Fid> --evidence "…"   # red half on its own surface (waive-red --reason = machine exemption)
lzy dag dependents <id|surface>         # "what depends on X" (central ledger, read-only)
lzy dag stale                           # invalidation preview vs current fingerprint (display-only)
lzy attest comparator --file <json>     # comparator verdicts as attestation (HEAVY finish enforces MATCH)
lzy policy show|explain                 # policy identity & obligation set explainer (0.4.0 M1; v1 goals print "not applicable")
lzy gate explain                        # unified read-only gate: per-obligation verdicts + snapshot hash (blocked exits non-zero)
lzy review run [--duty <id>] [--timeout-ms N] [--ack-metering]   # controlled independent review runner (0.4.0 M2): isolated HOME +
                                        # candidate snapshot, structured verdict on record; 0=pass&valid / 1=blocked·invalid / 2=usage / 3=precondition (no spawn)
lzy review cleanup <runId>             # development: reclaim dead-owner credential copies/tmp; keep transcripts/usage DB
lzy review list                         # enumerate review runs on record (read-only; corrupt family fails closed non-zero)
lzy review recheck [--fingerprint <p8>] # independent recheck run (0.4.0 M3): fresh same-duty session + closure candidates
lzy review qualify <runId> --scope <decl.json>  # scope qualification (0.4.0 M4): declare the duty's scope and run the
                                        # adversarial challenge suite (ten axes); GRANTED (0) / REJECTED-but-recorded (1); zero points (no spawn)
lzy review reuse <runId>                # reuse applicability (0.4.0 M4): classify the current candidate against the base
                                        # snapshot path by path; all-unrelated = applicable (0) / any declared, unknown or
                                        # structural-axis drift = fallback with per-reason lines (1); the base run and
                                        # qualification records stay byte-identical (append-only judgment)
lzy finding list|show|resolve-request|close|diagnose|relink|reopen
                                        # findings ledger (0.4.0 M3/M4): unclosed blocking findings survive
                                        # reset/supersede; closure only via resolve-request → recheck → close;
                                        # reopen = controlled re-entry when the closure basis went stale
                                        # (closed → resolve-requested, append-only events, history preserved)
lzy review show <runId>                 # field-by-field + findings table + raw output pointer (read-only)
lzy loop finish                         # the final gate: all done + fresh evidence + all trees clean (+ HEAVY: MATCH attestation); writes the final attestation and auto-archives
lzy loop export                         # re-export the evidence bundle
lzy loop handoff --snapshot <file>      # register a clean handoff; next Stop releases once
lzy loop cost                           # points report (standing coefficients + promo overlay, read-only)
lzy loop list [--root <dir>]            # read-only sweep of sibling repos' goal loops
lzy loop history                        # goal lineage (evidence ∪ stubs ∪ trailers, read-only)
lzy loop abandon | lzy loop reset       # give up / clear state

The plan gate

Plans are markdown checklists with two item kinds:

- [N1] Implement the export endpoint in src/api/export.ts
- [N2] Add CSV serialization with quoting rules
deps: N1
- [F1] `curl localhost:3000/export` returns 200 and parses as CSV (qa-executor)

Known unknowns

Decision-complete means no undecided choices — not no unverified premises. A HEAVY plan therefore ends with an explicit known-unknowns section: 1–3 assumptions the plan silently rests on, each with a falsification path (the signal that would prove it wrong, and how to check). Writing “none” is allowed, but must say in one line where you looked.

The section complements the plan gate rather than softening it: it states unverified premises, not postponed decisions — and the gate still scans every line of it, so an actual TBD inside the section is rejected like anywhere else. The plan-reviewer audits it against hidden risks: is every entry falsifiable, and is a “none” credible?

Evidence discipline

The continuation budget

ZCode grants each session a shared pool of 3 stop-continuations, which background task notifications also draw from. LazyZCode’s Stop hook spends at most 2 per session, deliberately reserving 1. Counters are isolated per sessionId (a probe that counted globally once hijacked an unrelated session — that constraint is now structural). Any hook error fails open: the loop never traps a session it does not own, and the user’s explicit stop always wins once the budget is spent.

SessionStart re-injects the loop state, so a fresh session continues where the previous one stopped.

Project memory

Planning heavily against a codebase nobody has mapped wastes the plan gate. lazyzcode:init-deep, the plugin’s second skill, builds layered project memory: a root AGENTS.md map plus maps for qualifying subdirectories (build entry points, directories with 40+ files, directories the root already mentions). ZCode reads AGENTS.md natively, so the map rides into every future session — no hook required.

The skill is conservative by contract: it drafts first and writes only after your approval; existing files receive patch suggestions, never overwrites; secrets and machine-local paths stay out. It touches only the AGENTS.md layer — personal lessons go to the host’s native memory at a zw finish.

lzy agents-md lists qualifying directories and coverage gaps; lzy doctor’s agents-md line audits adoption (warn-only: a repo without a root map is skipped, not nagged). HEAVY planning consults the map when one exists.

Unattended

Unattended mode is the same goal loop on a schedule. The engine’s built-in automation wakes a fresh session with zw continue (unattended: …); the wake cadence is off-peak by design — lzy doctor’s schedule line derives an 8-hour window opposite your measured rate-limit concentration hours, then cross-checks it against known platform pricing peaks (GLM / DeepSeek, a static UTC+8 table maintained by hand) and always states the pricing-safe nightly window. The table can go stale — treat the line as measured rate-limit data plus declared pricing, not a guarantee.

The guardrail is structural, not a promise: an unattended session continues the open goal only — it never registers a new goal and never writes or adopts a plan, because the decision-complete gate needs a human. With nothing to continue it exits cleanly, and the wake-up itself is bounded by the continuation budget, fatal-429 handling, serial subagents, and ≥1-hour spacing between runs.

Since 0.2.0 a wake-up also has an in-wake execution channel: lzy loop drive spawns headless engine segments inside one wake and pushes the executing goal segment by segment. Between segments it checks the gates — the run holds a minutes-scale lease (one runtime holder at a time, renewed by heartbeat), each run gets a fresh wall-clock + points budget (the points axis meters this run’s own per-segment session usage — the account-level rolling waterline is a lzy doctor advisory only), and HIGH+ risk goals are machine-rejected before anything spawns. Every segment’s lzy writes carry the run’s fence token, so a taken-over run fails closed on write instead of corrupting loop state. Wind-down is clean and enumerated: done, budget exhausted, segments exhausted, two zero-progress segments (stuck) — plus two wind-down causes that exist only in the dormant H3R prototype’s lane (they never fire with the prototype asleep): the step gate stopping at a high-risk step (h3r), or a high-risk command denied at the tool boundary (PreToolUse) — every non-done wind-down writes the 7-field handoff snapshot itself and registers the marker for the next session. lzy doctor’s drive line reports the channel’s availability on your machine:

lzy loop drive [--wall-ms N] [--max-segments N] [--mode m] [--workers N | --fast]

Workers (0.2.4). lzy loop drive --workers N (or the --fast sugar, N=2) runs N worker chains per wave on sibling worktrees: steps are split by the claim gate, each worker commits inside its own worktree, and the wave ends with a merge back plus a barrier re-anchor of all evidenced F items. A merge conflict winds down cleanly and leaves the branches for a human. Entry requires env-auth and the H3R wake switches to be off; the measured cost is turns ≈2× with small tasks possibly slowing down — read the experiment report (docs/reviews/2026-fast-exp-report.md §1.3) before enabling.

The host automation stays the only scheduled wake face — drive is what runs once awake (and what you can run yourself in an unattended window).

No need to wait for your laptop for the overnight digest: push the results to your own IM bot webhook (zero servers — lzy ships no notification channel of its own). Recipe: unattended notify.

Discipline agents

Three read-only roles ship in the plugin’s agents/ directory and are auto-discovered by the engine. Spawn them via the Agent tool with the role as subagent_type:

Role Use it for
lazyzcode:explorer Pre-plan reconnaissance: map the relevant code area, answer the planner’s questions with file:line evidence. Can use a codegraph index on large repos.
lazyzcode:plan-reviewer The plan review gate: audits decision-completeness, N/F well-formedness, real F-item surfaces, hidden risks. Returns VERDICT: PASS or VERDICT: REVISE.
lazyzcode:qa-executor Evidence execution: run one F-item verification on the real surface and report exactly what it observed — commands and raw output. It fights evidence fabrication.

All three are read-only: they never edit code, and their value is that their findings come from the repository, not from the main agent’s assumptions.

Hooks & lifecycle

Hook Event Does
session-start.js SessionStart Re-injects goal-loop state so a new session picks up where the last stopped.
trigger.js UserPromptSubmit Stratified trigger matching; injects the zw bootstrap on invocation.
comment-checker.js PostToolUse (Edit/Write) Advisory detection of TODO/FIXME/XXX/HACK markers and debug residue (console.log, console.debug, debugger) in new content. Capped at 5 hits, 300 characters, inject-only — and only active in workspaces with an open goal loop.
stop.js Stop Requests continuation (max 2/session) with the remaining-steps context while a loop is open; consumes a registered handoff marker once and releases without spending the budget.
tripwire.js PostToolUseFailure Same-tool failure-streak tripwire: warns once when a tool keeps failing or firing off-target (the misfire-attractor escape hatch); inject-only.
h3r-pretool.js PreToolUse (Bash) The command-layer H3R gate, dormant by default: inside an unattended lzy loop drive segment (the drive-injected LZY_SEGMENT_ID is present) and awake only when LZY_ABLATE_H3R_PRETOOL is 1 — the inverse of the LZY_ABLATE_* family — it denies Bash commands whose text matches the H3R word list and writes the hit marker drive consumes for a clean wind-down (7-field snapshot, exit 0). Interactive sessions carry no drive-injected segment id — exempt given env hygiene (the gate validates the <int>:seg-<int> shape, so a stray export degrades to no gate), and they are the recovery path (ADR-0022).

All six commands route through plugin/hooks/run-hook: the engine spawns hooks with its own environment, and a GUI-launched ZCode may have no node on PATH. One manifest line serves both platform families — POSIX shells execute the extensionless run-hook script, while on Windows cmd.exe resolves the same name to the run-hook.cmd twin via PATHEXT. The launcher falls back to nvm (highest version) and Homebrew locations on POSIX (nvm-windows and Program Files on Windows), logs to /tmp/lzy-hook-launcher.log (%TEMP%\lzy-hook-launcher.log on Windows), and exits 0 (fail-open) if everything is missing. lzy doctor’s hook-node check reports the resolved path.

Rate-limit discipline

GLM plans enforce an account-level concurrency limit ([1302] / 429). There is no fixed threshold you can configure around: measured headroom varies by plan tier, time of day, and account state — lzy doctor measures it from your engine logs (last 2 days) and reports deduplicated 429 turns, fatal turns, the longest sustained run, and — when the data is coherent — an empirical concurrency band (“N sessions clean / M sessions collide”). Warn only; it never flips the exit code.

Behavioral rules the zw skill carries:

Unattended (host automation): scheduling and the continue-only protocol live in Unattended; the window comes from lzy doctor’s schedule line — your own measured concentration hours cross-checked against declared pricing peaks, not a guess.

CLI reference

Project memory: the lazyzcode:init-deep skill generates a layered AGENTS.md map (root + qualifying subdirectories), drafts first — nothing is written without your approval; lzy doctor patrols coverage on every run.

lzy install                     deploy plugin + registry + official enable
lzy sync [--watch]              re-deploy payload (hot reload; --watch keeps watching)
lzy status                      quick health check (exit 0 = no fail-level findings)
lzy doctor                      deep local diagnostics (zero telemetry)
lzy uninstall                   remove cache + registry entry
lzy loop register <slug> --title <t> [--tier heavy]   create the goal (planning; HEAVY adoption without PASS review is machine-rejected)
lzy loop plan <file> [--review <v>] [--force]   adopt the N/F checklist (snapshots plan + binds planHash)
lzy loop supersede <file> [--review <v>]   forward-only plan change mid-execution (supersedes the attempt, opens attempt+1, gates re-run)
lzy loop start                  planning → executing; records base tree hash + 并发纪律 advisory
lzy loop subject add <path>     declare sibling repo root (executing-only; validated, dedup'd)
lzy loop subject remove <path>  remove a subject (missing-deadlock escape)
lzy loop subject list           list the subject set
lzy loop tier heavy             tier upgrade, one-way (adoption-time machine gate)
lzy loop claim [<id>] [--release]  per-step claim (multi-worker; blocked-step checks; 48h mutex)
lzy loop risk <level>              risk_class upgrade (low|med|high|restricted; one-way; ADR-0020)
lzy loop lease acquire|heartbeat|release|reclaim  run-level lease (minutes-scale mutex; fence token;
                                  reclaim = zombie-lease exit; ADR-0020)
lzy loop budget init|spend|remaining  drive budget (wall-clock + points double cap; ADR-0020)
lzy loop status                 progress, next step, evidence freshness, tier/subjects/snapshot
lzy policy show|explain         policy identity & obligation set explainer (0.4.0 M1; v1 goals print "not applicable")
lzy gate explain                unified read-only gate: per-obligation verdicts + snapshot hash (blocked exits non-zero)
lzy review run|list|show|recheck|qualify|reuse  controlled independent review runner + scope qualification/reuse (0.4.0 M2/M3/M4):
                                run = real review session on isolated HOME + candidate snapshot, verdict on record
                                (0=pass&valid/1=blocked·invalid/2=usage/3=precondition, no spawn);
                                list/show = read-only enumeration & field view; recheck = independent re-verification
                                + close-candidate reconciliation; qualify = declare scope & run the ten-axis
                                adversarial suite; reuse = path-by-path reuse applicability
lzy finding list|show|resolve-request|close|diagnose|relink|reopen
                                findings ledger (0.4.0 M3/M4): unclosed blocking findings block finish and survive
                                reset/supersede/rename; closure only via resolve-request → recheck → close (basis
                                required); two consecutive invalid fixes flip diagnosis-required (diagnose resets);
                                reopen = controlled re-entry when the closure basis went stale (closure-basis-stale)
lzy loop verify                 evidence freshness audit (exit 1 = stale/unbound/no goal); per-tree head/dirty lines
lzy step done <ID> [--note <t>] [--evidence <t>] [--evidence-file <f>]…
lzy loop finish                 final gate: all done + fresh composite fingerprint + all {host}∪subjects trees clean; atomic archive
lzy loop export                 re-export the evidence bundle (<slug>.report.md)
lzy loop cost                   points report (coefficients + promo overlay, read-only)
lzy loop list [--root <dir>]    cross-repo goal-loop sweep (read-only)
lzy loop history                goal lineage (evidence ∪ stubs ∪ trailers, read-only)
lzy loop attempts               attempt lineage (attempt.json ∪ central ledger derivation, read-only)
lzy loop abandon                give up, keep the record
lzy loop reset                  clear loop state (incl. session counters, orphan tmp)
lzy agents-md                   layered AGENTS.md audit (exit 1 = gaps/overcaps)
lzy version                     print version

Flags: --note is capped at 300 chars, --evidence at 4000, --evidence-file is repeatable (≤4 per F item, each ≤20 MB); --force=true and --force are equivalent; --watch=<value> warns (it takes no value).

Diagnostics

lzy status runs the base checks; lzy doctor runs those plus the deeper ones. Exit code: fail findings flip it to 1; warn and skip never do.

Check Meaning
payload Repo plugin manifest readable
install Registry entry present and healthy
files Deployed cache matches the repo payload, file by file
enabled The engine lists the plugin as enabled
codegraph Codegraph MCP/CLI availability (absence = skip)
loop Goal-loop progress in this directory (skip when none; warns while a loop is open)
hooks Hook syntax self-check (vm-parsed in a worker) + hooks.json registry validation
node Node version floor (≥ 24)
hook-node Which path the hook launcher resolves node from
lzy-path Whether lzy resolves on PATH
state .lazyzcode/ hygiene (orphan temp files, goal state)
claims Claim patrol: who claimed the open goal loop, stuck markers; zero claims = nobody is pullable under claim-gated pull-back (warn, never flips the exit code)
host-git Host workspace is a git repository (warn with git init guidance when not — evidence binds git trees, ADR-0019)
handoff Handoff marker present (the next Stop consumes it and releases)
handoff-usage Anonymous release counters, registered/consumed (survive reset)
ledger Commit-ledger patrol: share of goal-era commits missing the Goal: trailer (warn, never flips the exit code)
waterline Rolling 5-hour point burn vs the self-calibrated nudge threshold (degraded note when sqlite3 is absent)
orphan-wake Idle-burn patrol for unbound wake automations anchored here (skip when none mounted)
h3r-words The H3R word-list payload: present, schema-valid, and the same order the CLI’s reader yields. Missing = warn — awake-time behavior splits by reader: the drive-side gate hard-rejects (throws); the command-layer hook fails open (ADR-0022’s failure semantics)
lock Lock-contention window: acquisitions / how many had to wait / total and max wait / wait timeouts (measured against LOCK_WAIT_MS; skip with no samples; timeouts > 0 means §⑩-4’s pre-registered trigger fired — warn). Readings are lower bounds: the counters ride a lock-free read-merge-write, and the moment a delta is most likely to be lost is the moment contention is heaviest
platform Engine-candidate notice per platform (ok + path when the desktop engine is found)
agents-md Layered AGENTS.md coverage audit + staleness hint (≥50 covered-dir commits since the map’s last commit; skip when no root file; lzy agents-md for details)
rate-limit GLM plan 429 pressure from the last 2 days of engine logs
transport Transport deaths (request-never-reached-server failures, e.g. ENETDOWN): counted as a separate family, never fed into the concurrency math
content Content-moderation kills (provider content-filter mid-stream kills, e.g. 1301): counted as a separate family; an in-place retry reproduces, never fed into the concurrency math
band-by-provider Per-provider empirical band (completed-side clean × 429 dirty buckets); emitted only when the window has ≥1 429 and ≥2 providers — providers without 429s of their own are honestly labeled “no dirty-face sample”
cost Model-tier advisory (“success → try lighter tier”): a zero-429 window with a low rolling waterline suggests a lighter tier for routine goals; advisory text only
schedule Off-peak advisory: automation window from the measured concentration, cross-checked against declared pricing peaks with a safe-window note; hand-maintained UTC+8 pricing table (skip without evidence)

State & configuration

LazyZCode has no configuration file. Everything is derived:

Path Contents
.lazyzcode/loop/goal.json Current goal: steps, statuses, evidence bindings
.lazyzcode/loop/sessions/<sessionId>.json Per-session Stop-hook counters
.lazyzcode/loop/salvage/<slug>.md Salvageable-artifact stubs written when a loop is reset/abandoned (uncommitted changes, footnoted commits, asset pointers); lzy loop status surfaces them in both the no-goal and goal-present views
.lazyzcode/plans/<slug>.md Plan documents
.lazyzcode/evidence/ Archived evidence material
engine plugin cache Deployed payload (managed by lzy install/sync)
LazyZCode registry Install records (managed by the installer)

Environment: LZY_ZCODE_ENGINE replaces the default engine candidate list (default: /Applications/ZCode.app/Contents/Resources/glm/zcode.cjs). That is the entire configuration surface.

Privacy: zero telemetry; diagnostics are computed locally and printed locally.

Compatibility & limitations

Security & trust surface

What LazyZCode runs on your machine, where it installs, and what it deliberately does not defend against:


LazyZCode is MIT-licensed. The workflow is inspired by lazycodex (MIT); oh-my-openagent (OmO, SUL-1.0) contributed ideas only. Documentation structure follows the lazycodex docs with credit.