Under the hood

For developers who want to extend LazyZCode, contribute to it, or integrate with it. Companion to the Documentation: there the workflow, here the machinery. Everything on this page is derived from the actual source (plugin/, core/, cli/) — no aspirations, just what ships.

System architecture

LazyZCode is three pieces with hard boundaries: a plugin the engine loads, a CLI that owns the loop state, and a state directory your project keeps.

ZCode engine lazyzcode:zw plugin lzy CLI .lazyzcode/ git repository desktop app · zcode.cjs skills/zw · orchestration text hooks ×6 · via the run-hook launcher agents ×3 · read-only roles goal-loop state machine goal.json · plans · evidence HEAD^{tree} loads plugin fires hook events official plugins enable · config.json untouched reads / writes evidence binds HEAD^{tree}
Boundaries are deliberate: the installer never writes your config.json (enabling flows through the engine's official plugins enable), the loop state lives in the project, and the plugin ships zero runtime dependencies.

Design rule of thumb, in order: Skill > MCP > Tool > Hook. Anything that survives being offline lives in skill text; hooks are the last resort and are written to fail open.

The goal loop, as a state machine

lzy loop is a plain state machine on disk (no daemon). The skill text drives the model through it; the CLI gates every transition.

register plan gate executing finish gate done decision-complete HEAVY: reviewer PASS Stop hook pulls back · ≤2 per session lzy step done · F items carry evidence
Every transition is a CLI command; every gate is a check that can refuse. The Stop hook is the only thing that can push the model back to work, and it is budget-capped and fail-open by design.
lzy loop register <slug> --title "…"
lzy loop plan <plan.md> --review "plan-reviewer: PASS …"
lzy loop start
lzy step done <ID> --evidence "curl /export -> 200, parses as CSV"
lzy loop finish

Evidence binding

The core trick of the whole product: proof is bound to the content snapshot of a commit, so it cannot quietly rot.

commit A · tree 95a0… commits B… · tree c3f1… commit C · tree 7d22… F-item evidence captured bound: 95a0… finish re-checks freshness evidence from 95a0… is stale on 7d22…
Commit first, capture after. The moment the code changes, old evidence is stale by construction and lzy loop finish refuses it; lzy step done re-binds on recapture.

Two deliberate consequences: .lazyzcode/ itself never counts as a dirty worktree, and "tests are green" is just one surface among several, never a substitute for the named F-item surface.

Hook lifecycle

Six hooks ride the engine's session timeline. All of them spawn through plugin/hooks/run-hook, which resolves node from PATH, then nvm, then Homebrew on POSIX (Windows resolves the same manifest line to the run-hook.cmd twin via PATHEXT and falls back to nvm-windows/Program Files), so a Dock-launched ZCode (whose hook environment has no node) still works; the fallback path is reported by lzy doctor's hook-node check.

SessionStart UserPromptSubmit PreToolUse PostToolUse PostToolUseFailure Stop re-inject loop state into fresh sessions trigger match → inject zw bootstrap command-layer H3R gate (dormant prototype) comment-checker nudge (Edit / Write) tripwire: same-tool fail streak → warn once request continuation ≤2 · handoff release session-start.js · trigger.js · comment-checker.js · h3r-pretool.js · tripwire.js · stop.js — all spawned via the run-hook launcher
The engine exposes 7 hook events and a shared pool of 3 stop-continuations that background notifications also draw from; LazyZCode spends at most 2 per session and always fails open.

Extending it

Add a discipline role. Drop a Markdown file with frontmatter into plugin/agents/; the engine auto-discovers the directory. Roles are read-only contracts: explorer (recon with file:line evidence), plan-reviewer (VERDICT: PASS | REVISE), qa-executor (observed raw output, never inference). Keep any new role's output contract machine-checkable.

Trigger matching lives in plugin/hooks/trigger.js, stratified on purpose:

Pattern Fires
/^\s*zw(?![a-z0-9_-])/i start of prompt only
/lazyzcode[::]zw/i anywhere, full-width colon included
`/(^ [^a-z0-9_-])(ulw

Hook output contract: only the Stop hook can continue a session, and only with {continue:true, additionalContexts:[non-empty]} (the engine's own contract); the other four hooks emit {additionalContext} — inject-only. Anything else, including a crash, fails open and never traps the session.

Configuration surface: one environment variable. LZY_ZCODE_ENGINE replaces the engine candidate list; everything else is derived from the repo and engine state.

Build, test, preview

npm test                      # node:test contract suite, zero deps
cd scripts/docs-preview       # dev-only toolchain (own package.json)
npm install
npm run build                 # docs/ -> dist/ (Jekyll emulation)
npm run check                 # link crawl + anchor integrity, exit 1 on any miss

CI runs the suite on node 22 / 24. The docs site builds on GitHub Pages from /docs (Jekyll, GFM); scripts/docs-preview/build.mjs mirrors that pipeline locally — Pages adds rouge syntax coloring on top, everything else matches.

Compatibility notes


LazyZCode is MIT. Workflow inspired by lazycodex (MIT); OmO (SUL-1.0) ideas only. Docs structure follows the lazycodex docs with credit.