Skip to content

Commit 0a73f08

Browse files
SamPlvsSam Tukraclaude
authored
feat(enforcement): v2 Phase 1 — WS-A enforcement plane (#107)
* docs(v2): adopt v2 rearchitecture plan from 3-repo deep-dive review Adopt all 12 features synthesized from full-source reviews of oh-my-claudecode (yeachan-heo), ruflo (ruvnet), and ralph (snarktank), organized into five layer-based workstreams across six gated phases. - plans/zo-v2-rearchitecture.md: 20-check tiered oracle, anti-scope, reference repo links (+ .gitignore exception) - docs/reference/v2-rearchitecture.mdx: full feature table w/ provenance - docs/roadmap.mdx: v2 section (4 pillars + repo credits) - website: new §11 'What's next' crediting the three source repos - memory: STATE session 040, DECISION_LOG v2 decision, research corpus at memory/zo-platform/research/2026-08-12-repo-reviews/ Website change verified via HTML balance check + static-server DOM inspection; Astro build deferred to CI (no Node.js on this machine). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(enforcement): v2 Phase 1 — WS-A enforcement plane (contracts, drift guard, memory hooks, sealed paths, nonce gates) Implements all five WS-A features from plans/zo-v2-rearchitecture.md: - contracts.json emitted at decompose (src/zo/contracts.py) with concrete deliverable paths; SubagentStop hook blocks agents stopping with unmet deliverables (missing/undersized/pattern/empty-dir checks) - workflow-drift-guard Stop hook: completion claim in last message + added TODO/FIXME/NotImplementedError diff lines -> block (ZO_DRIFT_GUARD=0 opt-out) - PreCompact/SessionEnd/PostToolUseFailure hooks mechanize the memory protocol (STATE flush, summary backfill, structured failure JSONL feed) - sealed-paths PreToolUse guard: memory-root control files + user prefixes denied; per-agent off-limits write scopes enforced from contracts.json - nonce-verified gates end to end: minted at GATED, required by apply_human_decision (PermissionError otherwise, single-use), new 'zo gates approve/reject --nonce' CLI, /approve+/reject slash commands rewritten to route through it (forgeable hand-edit path closed) One shim (.claude/hooks/zo-hookkit.sh -> python3 -m zo.hookkit) routes all six hook events; every handler fail-open. Plan check 6 amended to path-scoped verifier enforcement (no disallowedTools frontmatter exists; rationale in DECISION_LOG). Tests: 854 -> 904 passed / 7 skipped (+50, incl. the repo's first hook-script tests: subprocess + stdin JSON). ruff src/ clean. validate-docs green. Seeded-failure tests cover plan oracle checks 1-5, 7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(enforcement): hook-trace observability + live-session verification evidence - zo.hookkit: one JSONL trace line per hook invocation (event, stdin keys, agent identity, emitted-output flag) to logs/hook-trace-{date}.jsonl; ZO_HOOK_TRACE=0 disables. This is how the enforcement plane's firing is observable in live sessions (handlers are silent unless they block). - drift-guard: prefer the live Stop payload's last_assistant_message (verified present in the 2026-08-12 live trace); transcript parse kept as fallback. - Live verification evidence (DECISION_LOG 15:30 entry): sealed-paths DENIED a real Write to gate_mode in a live session; drift-guard fired correctly-silent on a real Stop; subagent-stop fired with agent_type + agent_id in the live payload — resolves the Phase-1 agent-identity question (per-agent contract enforcement keys correctly). - PR-046 prior: this Desktop-managed Mac has no claude CLI/uv/npm; hook shims must run on bare python3 (venv fallback is load-bearing). Tests: 904 -> 908 passed / 7 skipped. ruff src/ clean. validate-docs green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Sam Tukra <sam101fe4x@Sams-MBP.Home> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent ac07a26 commit 0a73f08

24 files changed

Lines changed: 2047 additions & 99 deletions

.claude/commands/gates/approve.md

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,43 @@
22
description: Approve the current pending gate and advance to the next phase
33
---
44

5-
# /approve — Gate Approval
5+
# /approve — Gate Approval (nonce-verified)
66

77
You are executing a human gate approval for the current Zero Operators project.
88

9+
Gate approvals are **nonce-verified** (v2 WS-A5): a single-use approval nonce
10+
is minted when a phase reaches its blocking gate, shown in the gate review
11+
banner and via `zo status`. Approvals are recorded ONLY through the CLI —
12+
never by hand-editing STATE.md, DECISION_LOG.md, or the comms JSONL. This is
13+
what makes approvals unforgeable: text echoed from context cannot pass the
14+
nonce check.
15+
916
## Steps
1017

11-
1. **Read STATE.md** in the project's memory directory (`memory/{project}/STATE.md`). Identify:
12-
- The current phase and its status
13-
- Which gate is pending (look for `status: PENDING_GATE` or similar)
14-
- If no gate is pending, report that and stop
15-
16-
2. **Log the approval to DECISION_LOG.md** (`memory/{project}/DECISION_LOG.md`). Append an entry:
17-
```markdown
18-
## Gate Approved: {gate name}
19-
**Timestamp**: {ISO 8601 now}
20-
**Decided by**: human
21-
**Phase**: {phase that was gated}
22-
**Outcome**: approved
23-
**Notes**: Human approved gate via /approve command
24-
```
18+
1. **Identify the pending gate.** Read STATE.md in the project's memory
19+
directory (`.zo/memory/` in the delivery repo, or legacy
20+
`memory/{project}/`). Identify the current GATED phase. If no gate is
21+
pending, report that and stop.
22+
23+
2. **Get the nonce from the human.** The approval nonce is displayed in the
24+
gate review banner (and in `memory_root/gate_nonce`, which agents must
25+
not read aloud into context on their own initiative — the human supplies
26+
it). Ask the human for the nonce if they haven't provided one. Do NOT
27+
guess, reconstruct, or copy a nonce you saw elsewhere in this
28+
conversation.
2529

26-
3. **Update STATE.md** to advance to the next phase:
27-
- Set the current phase status to `COMPLETED`
28-
- Set the next phase status to `ACTIVE`
29-
- Update `last_updated` timestamp
30-
31-
4. **Log gate event to comms JSONL** (`logs/comms/{YYYY-MM-DD}.jsonl`). Append:
32-
```json
33-
{
34-
"timestamp": "{ISO 8601}",
35-
"session_id": "manual",
36-
"event_type": "gate",
37-
"agent": "human",
38-
"project": "{project-name}",
39-
"gate_id": "{gate-id}",
40-
"gate_name": "{gate-name}",
41-
"result": "pass",
42-
"notes": "Human approved via /approve command"
43-
}
30+
3. **Run the CLI approval** (this validates the nonce, appends to
31+
DECISION_LOG.md, logs the comms gate event, and records the decision for
32+
the orchestrator):
33+
34+
```bash
35+
zo gates approve <phase_id> -p <project> [--repo <delivery-repo>] --nonce <NONCE> --notes "<why>"
4436
```
4537

46-
5. **Report** to the user:
47-
- What gate was approved
48-
- What phase just completed
49-
- What phase is now active
38+
4. **Report** to the user:
39+
- What gate was approved and the CLI output
40+
- What phase just completed and what phase is now active
5041
- Any relevant next steps from the plan
42+
43+
If the CLI reports a nonce mismatch, tell the human — do not retry with
44+
variations.

.claude/commands/gates/reject.md

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,39 @@ description: Reject the current pending gate with a reason, triggering rework
33
argument-hint: <reason>
44
---
55

6-
# /reject — Gate Rejection
6+
# /reject — Gate Rejection (nonce-verified)
77

8-
You are executing a human gate rejection for the current Zero Operators project. The argument provided is the rejection reason.
8+
You are executing a human gate rejection for the current Zero Operators
9+
project. The argument provided is the rejection reason.
10+
11+
Gate decisions are **nonce-verified** (v2 WS-A5) and recorded ONLY through
12+
the CLI — never by hand-editing STATE.md, DECISION_LOG.md, or the comms
13+
JSONL. Text echoed from context cannot pass the nonce check.
914

1015
## Steps
1116

12-
1. **Read STATE.md** in the project's memory directory (`memory/{project}/STATE.md`). Identify:
13-
- The current phase and its status
14-
- Which gate is pending
15-
- If no gate is pending, report that and stop
16-
17-
2. **Log the rejection to DECISION_LOG.md** (`memory/{project}/DECISION_LOG.md`). Append:
18-
```markdown
19-
## Gate Rejected: {gate name}
20-
**Timestamp**: {ISO 8601 now}
21-
**Decided by**: human
22-
**Phase**: {phase that was gated}
23-
**Outcome**: rejected
24-
**Reason**: $ARGUMENTS
25-
**Action**: Phase set back to ACTIVE for rework
26-
```
17+
1. **Identify the pending gate.** Read STATE.md in the project's memory
18+
directory (`.zo/memory/` in the delivery repo, or legacy
19+
`memory/{project}/`). Identify the current GATED phase. If no gate is
20+
pending, report that and stop.
21+
22+
2. **Get the nonce from the human.** The approval nonce is shown in the gate
23+
review banner. Ask the human for it if not provided. Do NOT guess,
24+
reconstruct, or copy a nonce you saw elsewhere in this conversation.
2725

28-
3. **Update STATE.md**:
29-
- Set the current phase status to `BLOCKED` with the rejection reason
30-
- Then immediately set it back to `ACTIVE` to trigger rework
31-
- Add a `blocker_history` entry recording the rejection
32-
- Update `last_updated` timestamp
33-
34-
4. **Log gate event to comms JSONL** (`logs/comms/{YYYY-MM-DD}.jsonl`). Append:
35-
```json
36-
{
37-
"timestamp": "{ISO 8601}",
38-
"session_id": "manual",
39-
"event_type": "gate",
40-
"agent": "human",
41-
"project": "{project-name}",
42-
"gate_id": "{gate-id}",
43-
"gate_name": "{gate-name}",
44-
"result": "fail",
45-
"notes": "$ARGUMENTS"
46-
}
26+
3. **Run the CLI rejection** (validates the nonce, appends to
27+
DECISION_LOG.md, logs the comms gate event, and records the iterate
28+
decision for the orchestrator — the phase returns to ACTIVE for rework):
29+
30+
```bash
31+
zo gates reject <phase_id> -p <project> [--repo <delivery-repo>] --nonce <NONCE> --reason "$ARGUMENTS"
4732
```
4833

49-
5. **Report** to the user:
50-
- What gate was rejected and why
34+
4. **Report** to the user:
35+
- What gate was rejected and why (the CLI output)
5136
- The phase is now set back to ACTIVE for rework
5237
- What the agents need to address based on the rejection reason
53-
- Suggest next steps (re-run the phase, modify approach, etc.)
38+
- Suggested next steps (re-run the phase, modify approach, etc.)
39+
40+
If the CLI reports a nonce mismatch, tell the human — do not retry with
41+
variations.

.claude/hooks/zo-hookkit.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/bin/bash
2+
# zo-hookkit.sh — thin shim routing Claude Code hook events to zo.hookkit.
3+
#
4+
# Part of the v2 enforcement plane (WS-A, plans/zo-v2-rearchitecture.md).
5+
# Usage (from .claude/settings.json):
6+
# bash .claude/hooks/zo-hookkit.sh <event> 2>/dev/null || exit 0
7+
#
8+
# Fail-open by design: any missing precondition exits 0 silently. Blocking
9+
# and denials are emitted as JSON on stdout by zo.hookkit, never via exit
10+
# codes (matches the existing hook convention in this directory).
11+
set -uo pipefail
12+
13+
EVENT="${1:-}"
14+
[[ -z "$EVENT" ]] && exit 0
15+
16+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17+
REPO_ROOT="$(cd "$HOOK_DIR/../.." && pwd)"
18+
19+
# Only run in the ZO platform repo — delivery repos never carry ZO hooks
20+
# (specs/architecture.md isolation rule).
21+
[[ -d "$REPO_ROOT/src/zo" ]] || exit 0
22+
23+
# Prefer the project venv so pydantic imports resolve; fall back to system
24+
# python3 (zo.hookkit itself is fail-open on ImportError via the || below).
25+
PY="python3"
26+
[[ -x "$REPO_ROOT/.venv/bin/python3" ]] && PY="$REPO_ROOT/.venv/bin/python3"
27+
28+
# Pre-set ZO_REPO_ROOT wins (lets tests point the handlers at a sandbox).
29+
export ZO_REPO_ROOT="${ZO_REPO_ROOT:-$REPO_ROOT}"
30+
PYTHONPATH="$REPO_ROOT/src${PYTHONPATH:+:$PYTHONPATH}" "$PY" -m zo.hookkit "$EVENT" || exit 0
31+
exit 0

.claude/settings.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@
5959
"statusMessage": "Validating documentation consistency..."
6060
}
6161
]
62+
},
63+
{
64+
"matcher": "Write|Edit",
65+
"hooks": [
66+
{
67+
"type": "command",
68+
"command": "bash .claude/hooks/zo-hookkit.sh sealed-paths 2>/dev/null || exit 0",
69+
"timeout": 10
70+
}
71+
]
6272
}
6373
],
6474
"PostToolUse": [
@@ -81,6 +91,60 @@
8191
"type": "command",
8292
"command": "bash .claude/hooks/stop-check.sh 2>/dev/null || exit 0",
8393
"timeout": 10
94+
},
95+
{
96+
"type": "command",
97+
"command": "bash .claude/hooks/zo-hookkit.sh drift-guard 2>/dev/null || exit 0",
98+
"timeout": 20
99+
}
100+
]
101+
}
102+
],
103+
"SubagentStop": [
104+
{
105+
"matcher": "",
106+
"hooks": [
107+
{
108+
"type": "command",
109+
"command": "bash .claude/hooks/zo-hookkit.sh subagent-stop 2>/dev/null || exit 0",
110+
"timeout": 15
111+
}
112+
]
113+
}
114+
],
115+
"PreCompact": [
116+
{
117+
"matcher": "",
118+
"hooks": [
119+
{
120+
"type": "command",
121+
"command": "bash .claude/hooks/zo-hookkit.sh precompact 2>/dev/null || exit 0",
122+
"timeout": 15,
123+
"statusMessage": "Flushing state before compaction..."
124+
}
125+
]
126+
}
127+
],
128+
"SessionEnd": [
129+
{
130+
"matcher": "",
131+
"hooks": [
132+
{
133+
"type": "command",
134+
"command": "bash .claude/hooks/zo-hookkit.sh session-end 2>/dev/null || exit 0",
135+
"timeout": 15
136+
}
137+
]
138+
}
139+
],
140+
"PostToolUseFailure": [
141+
{
142+
"matcher": "",
143+
"hooks": [
144+
{
145+
"type": "command",
146+
"command": "bash .claude/hooks/zo-hookkit.sh post-tool-failure 2>/dev/null || exit 0",
147+
"timeout": 10
84148
}
85149
]
86150
}

docs/COMMANDS.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,26 @@ zo gates set MODE --project NAME
158158

159159
`MODE` is one of: `supervised` (human approves every gate), `auto` (orchestrator approves unless ambiguous), `full-auto` (all gates auto-approved).
160160

161+
### zo gates approve / reject
162+
163+
Record a nonce-verified human decision on the pending blocking gate (v2
164+
WS-A5). When a phase reaches its blocking gate, ZO mints a single-use
165+
approval nonce and shows it in the gate review banner. Decisions are only
166+
valid through these commands — hand-edited approvals cannot pass the nonce
167+
check, which is what makes gate passage unforgeable by agents.
168+
169+
```
170+
zo gates approve PHASE --project NAME --nonce NONCE [--notes TEXT]
171+
zo gates reject PHASE --project NAME --nonce NONCE --reason TEXT
172+
```
173+
174+
Both validate the nonce, append the decision to `DECISION_LOG.md`, log a
175+
comms gate event, and record the decision for the orchestrator (applied
176+
live at the next gate poll, or on `zo continue` for a fresh session).
177+
`reject` returns the phase to ACTIVE for rework with your `--reason`
178+
driving the next iteration. The nonce is cleared on use — replaying an
179+
old approval can never pass a later gate.
180+
161181
### zo watch-training
162182

163183
Live training metrics dashboard. Tails `logs/training/metrics.jsonl` in the delivery repo and renders a persistent Rich panel with epoch progress, loss/metrics table, checkpoint log, and loss sparkline. Auto-launched by `zo build` during Phase 4 via tmux split-pane.

docs/reference/v2-rearchitecture.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ architecture — features from different repos interlock into single mechanisms.
4646
| 1 | **Machine-checked deliverable contracts** — every agent's produced/consumed contract compiles to JSON (required files, sections, patterns) verified mechanically when the agent stops, plus a zero-cost "completion claim vs. TODO-stub diff" drift guard | oh-my-claudecode | P0 |
4747
| 3 | **Hook-enforced memory protocol** — STATE.md flushes before compaction, session summaries verified at session end, structured failure records captured automatically; the model can't forget because the model isn't asked | oh-my-claudecode | P0 |
4848
| 7 | **Anti-forgery gate approvals** — approvals only valid as structured tags carrying a per-verification nonce, so no agent can forge gate passage by echoing text | oh-my-claudecode | P1 |
49-
| 9 | **Structurally trustworthy verifiers**the oracle and reviewers mechanically cannot edit code (tool-level enforcement), evaluators emit a strict JSON contract, and evaluation files are sealed against modification by the loop they evaluate | oh-my-claudecode | P1 |
49+
| 9 | **Structurally trustworthy verifiers**verifier writes are mechanically confined to their contract scope (a PreToolUse guard denies writes into off-limits paths), evaluators emit a strict JSON contract, and oracle/control files are sealed against modification by the loop they evaluate | oh-my-claudecode | P1 |
5050

5151
### Workstream B — Control plane
5252

memory/zo-platform/DECISION_LOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,3 +1239,29 @@ The `--no-headlines` flag is preserved (not removed) for backwards compatibility
12391239
**Alternatives considered:** (1) Source-repo workstreams ("ralph track", "ruflo track") — rejected, provenance is not architecture; (2) P0-only scope — rejected by Sam, all 12 wanted; (3) waiting for the SDK refactor before substrate work — rejected, the fresh-context loop is the cheapest on-ramp to that refactor, not a competitor.
12401240

12411241
**Outcome:** Shipped: plan (+ gitignore exception), docs reference page + mint.json nav, roadmap v2 section, website §11 + drawer nav + §12 renumber, STATE/session-040/research-README memory updates. Verification honest note: **no Node.js available on this machine**, so the Astro build was NOT run locally — the website edit was verified via HTML tag-balance parsing and DOM inspection through a static file server (section renders, all 4 external links present, drawer link + renumbering correct); the deploy pipeline's build is the binding check. validate-docs run pre-commit per protocol.
1242+
1243+
## Decision: 2026-08-12T14:00:00Z
1244+
**Type:** FEATURE + ARCHITECTURE
1245+
**Title:** v2 Phase 1 (WS-A enforcement plane) — contracts, drift guard, memory hooks, sealed paths, nonce gates
1246+
1247+
**Decision:** Implemented all five WS-A features from plans/zo-v2-rearchitecture.md as a deterministic hook plane beneath the prompt plane: (1) machine-readable `contracts.json` emitted at decompose with concrete deliverable paths, validated by a SubagentStop hook that blocks agents stopping with unmet deliverables; (2) a zero-LLM workflow-drift-guard Stop hook (completion claim + added stub markers → block); (3) PreCompact/SessionEnd/PostToolUseFailure memory hooks mechanizing the "AUTOMATIC" memory protocol; (4) a sealed-paths PreToolUse guard denying writes to oracle/control state and enforcing per-agent off-limits scopes from contracts.json; (5) nonce-verified gate approvals end to end — minted at GATED, required by `apply_human_decision`, `zo gates approve/reject --nonce` CLI, slash commands rewritten to route through it. One bash shim (`zo-hookkit.sh`) routes all six hook events to `python3 -m zo.hookkit` so file mutation reuses MemoryManager's atomic/flock code paths.
1248+
1249+
**Rationale:** Direct implementation of the review's loudest theme ("enforcement plane below the prompt plane") and ZO's own PR-035 prior (aspirational contracts get ignored). Every mechanism ships wired (settings.json entries asserted by test) with a seeded-failure test proving it catches a planted violation — the "nothing ships unwired" rule from the plan's anti-scope.
1250+
1251+
**Plan amendment (check 6):** The plan called for "disallowedTools + PreToolUse" on verifiers. Recon showed (a) Claude Code has no disallowedTools frontmatter for subagents (tools: is an allowlist), and (b) verifiers require scoped write access (oracle-qa owns oracle/reports/, code-reviewer owns reviews/) — total Write removal would break their contracts. Amended check 6 to path-scoped enforcement: off-limits writes denied via contracts.json, keyed on agent identity in hook input, fail-open when absent. Honest limitation recorded: production identity presence depends on Claude Code's hook payload; verified in tests via seeded identity, to be confirmed live in Phase 6 e2e.
1252+
1253+
**Alternatives considered:** (1) tools: allowlist frontmatter on verifiers — rejected, cosmetic without Bash coverage and breaks legitimate writes; (2) six separate bash hook scripts — rejected for one parameterized shim (DRY, one venv-resolution path); (3) hard-blocking every stop while work remains (OMC's stop-engine) — explicitly rejected by the plan's anti-scope (fresh spawns + caps are the foundation; these hooks only catch violations).
1254+
1255+
**Outcome:** 854 → 904 passed / 7 skipped (+50: 19 contracts, 19 hookkit, 9 nonce, 8 shim integration — first tests ever to execute a .claude/hooks script), ruff src/ clean, validate-docs green. 4 pre-existing integration tests updated to supply the nonce (designed behaviour change). Cascade: docs/COMMANDS.md (+gates approve/reject), specs/memory.md recovery section rewritten to implemented reality, plan check 6 + docs feature #9 amended. Branch `claude/v2-phase1-enforcement` stacked on `claude/v2-rearchitecture-plan`.
1256+
1257+
## Decision: 2026-08-12T15:30:00Z
1258+
**Type:** VERIFICATION
1259+
**Title:** Live pre-PR verification of the WS-A enforcement plane (in-session, bypass-equivalent)
1260+
1261+
**Decision:** Verified the Phase 1 hooks firing in a REAL Claude Code session before merging PR #107, per Sam's direction. Method: added always-on hook-trace observability (one JSONL line per invocation to `logs/hook-trace-{date}.jsonl`: event, stdin keys, agent identity, whether output was emitted; `ZO_HOOK_TRACE=0` disables), then used the current live session itself as the test vehicle — the runtime picked up the new settings.json wiring mid-session.
1262+
1263+
**Evidence (from logs/hook-trace-2026-08-12.jsonl):** (1) `drift-guard` fired on a real Stop event, correctly silent — and the live payload carries `last_assistant_message` directly (drift-guard now prefers it; transcript parse kept as fallback). (2) `sealed-paths` fired on a real Write to `memory/zo-platform/gate_mode` and DENIED it — the lead agent of a live session was mechanically blocked from touching gate control state. (3) `subagent-stop` fired when a probe subagent stopped, and the live payload carries `agent_type` + `agent_id` + `agent_transcript_path` — RESOLVING the Phase-1 open question: per-agent contract enforcement will key correctly in live team sessions; the fail-open path is the exception, not the norm.
1264+
1265+
**Honest caveats:** PostToolUseFailure did not fire for nonzero-exit Bash commands in this session — its semantics appear limited to tool-infrastructure errors, so the failure feed will capture fewer events than designed; if nonzero-exit capture matters, add PostToolUse-with-error-inspection in WS-D. PreCompact/SessionEnd not yet observed live (no compaction occurred; SessionEnd fires at session close) — same wiring pattern as the three proven events. A full `zo build` demo run remains desirable on a machine with the claude CLI (this Desktop-managed Mac has none — see PR-046).
1266+
1267+
**Outcome:** 904 → 908 tests (3 trace + 1 live-payload drift-guard), ruff clean. Fail-open verified end to end in production conditions. Evidence committed to PR #107.

0 commit comments

Comments
 (0)