Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 31 additions & 37 deletions .claude/commands/gates/approve.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,43 @@
description: Approve the current pending gate and advance to the next phase
---

# /approve — Gate Approval
# /approve — Gate Approval (nonce-verified)

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

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

## Steps

1. **Read STATE.md** in the project's memory directory (`memory/{project}/STATE.md`). Identify:
- The current phase and its status
- Which gate is pending (look for `status: PENDING_GATE` or similar)
- If no gate is pending, report that and stop

2. **Log the approval to DECISION_LOG.md** (`memory/{project}/DECISION_LOG.md`). Append an entry:
```markdown
## Gate Approved: {gate name}
**Timestamp**: {ISO 8601 now}
**Decided by**: human
**Phase**: {phase that was gated}
**Outcome**: approved
**Notes**: Human approved gate via /approve command
```
1. **Identify the pending gate.** Read STATE.md in the project's memory
directory (`.zo/memory/` in the delivery repo, or legacy
`memory/{project}/`). Identify the current GATED phase. If no gate is
pending, report that and stop.

2. **Get the nonce from the human.** The approval nonce is displayed in the
gate review banner (and in `memory_root/gate_nonce`, which agents must
not read aloud into context on their own initiative — the human supplies
it). Ask the human for the nonce if they haven't provided one. Do NOT
guess, reconstruct, or copy a nonce you saw elsewhere in this
conversation.

3. **Update STATE.md** to advance to the next phase:
- Set the current phase status to `COMPLETED`
- Set the next phase status to `ACTIVE`
- Update `last_updated` timestamp

4. **Log gate event to comms JSONL** (`logs/comms/{YYYY-MM-DD}.jsonl`). Append:
```json
{
"timestamp": "{ISO 8601}",
"session_id": "manual",
"event_type": "gate",
"agent": "human",
"project": "{project-name}",
"gate_id": "{gate-id}",
"gate_name": "{gate-name}",
"result": "pass",
"notes": "Human approved via /approve command"
}
3. **Run the CLI approval** (this validates the nonce, appends to
DECISION_LOG.md, logs the comms gate event, and records the decision for
the orchestrator):

```bash
zo gates approve <phase_id> -p <project> [--repo <delivery-repo>] --nonce <NONCE> --notes "<why>"
```

5. **Report** to the user:
- What gate was approved
- What phase just completed
- What phase is now active
4. **Report** to the user:
- What gate was approved and the CLI output
- What phase just completed and what phase is now active
- Any relevant next steps from the plan

If the CLI reports a nonce mismatch, tell the human — do not retry with
variations.
66 changes: 27 additions & 39 deletions .claude/commands/gates/reject.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,39 @@ description: Reject the current pending gate with a reason, triggering rework
argument-hint: <reason>
---

# /reject — Gate Rejection
# /reject — Gate Rejection (nonce-verified)

You are executing a human gate rejection for the current Zero Operators project. The argument provided is the rejection reason.
You are executing a human gate rejection for the current Zero Operators
project. The argument provided is the rejection reason.

Gate decisions are **nonce-verified** (v2 WS-A5) and recorded ONLY through
the CLI — never by hand-editing STATE.md, DECISION_LOG.md, or the comms
JSONL. Text echoed from context cannot pass the nonce check.

## Steps

1. **Read STATE.md** in the project's memory directory (`memory/{project}/STATE.md`). Identify:
- The current phase and its status
- Which gate is pending
- If no gate is pending, report that and stop

2. **Log the rejection to DECISION_LOG.md** (`memory/{project}/DECISION_LOG.md`). Append:
```markdown
## Gate Rejected: {gate name}
**Timestamp**: {ISO 8601 now}
**Decided by**: human
**Phase**: {phase that was gated}
**Outcome**: rejected
**Reason**: $ARGUMENTS
**Action**: Phase set back to ACTIVE for rework
```
1. **Identify the pending gate.** Read STATE.md in the project's memory
directory (`.zo/memory/` in the delivery repo, or legacy
`memory/{project}/`). Identify the current GATED phase. If no gate is
pending, report that and stop.

2. **Get the nonce from the human.** The approval nonce is shown in the gate
review banner. Ask the human for it if not provided. Do NOT guess,
reconstruct, or copy a nonce you saw elsewhere in this conversation.

3. **Update STATE.md**:
- Set the current phase status to `BLOCKED` with the rejection reason
- Then immediately set it back to `ACTIVE` to trigger rework
- Add a `blocker_history` entry recording the rejection
- Update `last_updated` timestamp

4. **Log gate event to comms JSONL** (`logs/comms/{YYYY-MM-DD}.jsonl`). Append:
```json
{
"timestamp": "{ISO 8601}",
"session_id": "manual",
"event_type": "gate",
"agent": "human",
"project": "{project-name}",
"gate_id": "{gate-id}",
"gate_name": "{gate-name}",
"result": "fail",
"notes": "$ARGUMENTS"
}
3. **Run the CLI rejection** (validates the nonce, appends to
DECISION_LOG.md, logs the comms gate event, and records the iterate
decision for the orchestrator — the phase returns to ACTIVE for rework):

```bash
zo gates reject <phase_id> -p <project> [--repo <delivery-repo>] --nonce <NONCE> --reason "$ARGUMENTS"
```

5. **Report** to the user:
- What gate was rejected and why
4. **Report** to the user:
- What gate was rejected and why (the CLI output)
- The phase is now set back to ACTIVE for rework
- What the agents need to address based on the rejection reason
- Suggest next steps (re-run the phase, modify approach, etc.)
- Suggested next steps (re-run the phase, modify approach, etc.)

If the CLI reports a nonce mismatch, tell the human — do not retry with
variations.
31 changes: 31 additions & 0 deletions .claude/hooks/zo-hookkit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
# zo-hookkit.sh — thin shim routing Claude Code hook events to zo.hookkit.
#
# Part of the v2 enforcement plane (WS-A, plans/zo-v2-rearchitecture.md).
# Usage (from .claude/settings.json):
# bash .claude/hooks/zo-hookkit.sh <event> 2>/dev/null || exit 0
#
# Fail-open by design: any missing precondition exits 0 silently. Blocking
# and denials are emitted as JSON on stdout by zo.hookkit, never via exit
# codes (matches the existing hook convention in this directory).
set -uo pipefail

EVENT="${1:-}"
[[ -z "$EVENT" ]] && exit 0

HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$HOOK_DIR/../.." && pwd)"

# Only run in the ZO platform repo — delivery repos never carry ZO hooks
# (specs/architecture.md isolation rule).
[[ -d "$REPO_ROOT/src/zo" ]] || exit 0

# Prefer the project venv so pydantic imports resolve; fall back to system
# python3 (zo.hookkit itself is fail-open on ImportError via the || below).
PY="python3"
[[ -x "$REPO_ROOT/.venv/bin/python3" ]] && PY="$REPO_ROOT/.venv/bin/python3"

# Pre-set ZO_REPO_ROOT wins (lets tests point the handlers at a sandbox).
export ZO_REPO_ROOT="${ZO_REPO_ROOT:-$REPO_ROOT}"
PYTHONPATH="$REPO_ROOT/src${PYTHONPATH:+:$PYTHONPATH}" "$PY" -m zo.hookkit "$EVENT" || exit 0
exit 0
64 changes: 64 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@
"statusMessage": "Validating documentation consistency..."
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/zo-hookkit.sh sealed-paths 2>/dev/null || exit 0",
"timeout": 10
}
]
}
],
"PostToolUse": [
Expand All @@ -81,6 +91,60 @@
"type": "command",
"command": "bash .claude/hooks/stop-check.sh 2>/dev/null || exit 0",
"timeout": 10
},
{
"type": "command",
"command": "bash .claude/hooks/zo-hookkit.sh drift-guard 2>/dev/null || exit 0",
"timeout": 20
}
]
}
],
"SubagentStop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/zo-hookkit.sh subagent-stop 2>/dev/null || exit 0",
"timeout": 15
}
]
}
],
"PreCompact": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/zo-hookkit.sh precompact 2>/dev/null || exit 0",
"timeout": 15,
"statusMessage": "Flushing state before compaction..."
}
]
}
],
"SessionEnd": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/zo-hookkit.sh session-end 2>/dev/null || exit 0",
"timeout": 15
}
]
}
],
"PostToolUseFailure": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/zo-hookkit.sh post-tool-failure 2>/dev/null || exit 0",
"timeout": 10
}
]
}
Expand Down
20 changes: 20 additions & 0 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,26 @@ zo gates set MODE --project NAME

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

### zo gates approve / reject

Record a nonce-verified human decision on the pending blocking gate (v2
WS-A5). When a phase reaches its blocking gate, ZO mints a single-use
approval nonce and shows it in the gate review banner. Decisions are only
valid through these commands — hand-edited approvals cannot pass the nonce
check, which is what makes gate passage unforgeable by agents.

```
zo gates approve PHASE --project NAME --nonce NONCE [--notes TEXT]
zo gates reject PHASE --project NAME --nonce NONCE --reason TEXT
```

Both validate the nonce, append the decision to `DECISION_LOG.md`, log a
comms gate event, and record the decision for the orchestrator (applied
live at the next gate poll, or on `zo continue` for a fresh session).
`reject` returns the phase to ACTIVE for rework with your `--reason`
driving the next iteration. The nonce is cleared on use — replaying an
old approval can never pass a later gate.

### zo watch-training

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.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/v2-rearchitecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ architecture — features from different repos interlock into single mechanisms.
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |

### Workstream B — Control plane

Expand Down
26 changes: 26 additions & 0 deletions memory/zo-platform/DECISION_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1239,3 +1239,29 @@ The `--no-headlines` flag is preserved (not removed) for backwards compatibility
**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.

**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.

## Decision: 2026-08-12T14:00:00Z
**Type:** FEATURE + ARCHITECTURE
**Title:** v2 Phase 1 (WS-A enforcement plane) — contracts, drift guard, memory hooks, sealed paths, nonce gates

**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.

**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.

**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.

**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).

**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`.

## Decision: 2026-08-12T15:30:00Z
**Type:** VERIFICATION
**Title:** Live pre-PR verification of the WS-A enforcement plane (in-session, bypass-equivalent)

**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.

**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.

**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).

**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.
Loading
Loading