Skip to content

Epic: Phase 1 — Claude Code hook-based enforcement on writ #124

Description

@rmems

Current disposition — 2026-09-21

Superseded by #1's collaboration-first architecture. Keep any landed hook/safety code that protects real integrity invariants, but do not revive the enforcement-first phase epic or universal never-merge model.

Owner steering — 2026-09-19

The current product direction is #1: parallel subagent collaboration and conflict recovery, not expanding universal merge bans. This overrides conflicting enforcement-first acceptance below.

Preserve completed identity/worktree/recovery fixes. Do not reopen completed children or add blanket Edit/Write/path-overlap vetoes, no-merge hooks, merge-queue bans, or repeated approval machinery for their own sake. RM-145 owns simplifying shared docs and integration policy; #136 owns recovery-compatible coordination state. Local integration in an assigned worktree is distinct from a GitHub PR merge. The missing GitHub protection configuration is an operator concern to report accurately, not a reason to block all collaboration work.

Existing sessions and PRs stay in place. Historical scope follows for traceability; no new worker launch or repository-setting mutation is requested by this update.


Important

Product goal (2026-09-05): writ (formerly worktrees-hives) is the enforcement and admission-control layer for agent fleets. Task assignment is commoditized — Claude Code agent teams, /batch, Cursor /multitask and Codex already assign and isolate work. What nothing enforces is safe concurrent writing, and nothing arbitrates contention — whether a second agent may take a path or a work item already taken. writ enforces at the git mutation boundary through Claude Code hooks (a PreToolUse exit 2 cannot be overridden, even by another hook's permissionDecision: "allow"), leases worktrees it did not create, and never merges. Prefer work that hardens that boundary over creating worktrees or re-implementing task assignment.

Why this epic was rewritten

This was the stabilization epic. Its integration queue described PRs #130, #131, #132 as open at specific SHAs and #133/#137 as pending — all merged or closed by 2026-09-05. Its exit criteria pointed at #108 as the next epic; #108 is closed NOT_PLANNED. The stabilization goal succeeded: the PR queue is drained, 0 open PRs.

It is now the Phase 1 delivery epic for the pivot in #1.

Goal

Put the Rust enforcement core into the real production path as Claude Code hooks — on current crate names, with no rename and no deletion.

Renaming and demolition are deliberately excluded from this phase. If hooks land in the same PR as a rename, a failure is indistinguishable between "hooks broken" and "rename fallout." wh / wh-core / WH_* stay exactly as they are so every failure here is attributable to the new code. Rename is Phase 2, python/ deletion is Phase 3 — both tracked under milestone M2.

New CLI surface

wh hook <event>          # reads hook JSON on stdin, exits 0 or 2
wh verify <path>         # verify worktree identity + exact base
wh lease grant|check|release|list
wh install               # write the hook block into .claude/settings.json

wh hook is one entry point dispatching on hook_event_name:

Event Behavior
PreToolUse, matcher Bash Parse tool_input.command, run existing git_safe/gh_safe validation, exit 2 + stderr reason on reject
WorktreeCreate Verify exact base, write lease row. Non-zero exit aborts creation
WorktreeRemove Release lease row
SubagentStart / SubagentStop Upsert/retire agent-registry row

Matcher scope starts deliberately narrow

if conditions Bash(git *) and Bash(gh *) only. Expand only after false-positive burn-in on real sessions. A hook that blocks legitimate work gets uninstalled, and an uninstalled hook enforces nothing. Editing tools (Edit/Write) are Phase 5, after burn-in establishes that a blocking hook can be trusted.

Minimal lease store lands here, not in Phase 4

Without it, WorktreeCreate "records a lease" into nothing and we have reproduced the Markdown control plane in Rust. Scope for this phase:

  • rusqlite with the bundled feature — compiles SQLite from source, preserving single-static-binary distribution.
  • One file. A leases table (repo, branch, worktree path, owner, mode, TTL, heartbeat) and an agents table.
  • Modes: UNASSIGNED | WRITER_LOCKED | REVIEW_ONLY | NEEDS_HUMAN | BLOCKED | MERGE_READY.
  • Path scopes, budget columns, and the remaining tables are Phase 4; the schema must not need a rewrite to add them. Reserve nullable max_files, max_churn, max_fix_cycles, and fix_cycles on leases now — see [writ] Advisory change-growth budgets for collaborative agents #167.
  • SQLite's own locking supersedes the hand-rolled fcntl.flock + atomic-rename pattern in watchlist.py — do not port it.

crates/wh-core/src/state.rs is replaced by this store in this phase. It is currently a reader for a file with no writer, so wh status and wh jobs always return empty in real use.

Reproduced blockers (children, keep)

  • [Enforce] Reject ambiguous unqualified start-point refs #139 — ambiguous symbolic start points (P1). A same-named branch and tag at different commits make Git exit 0 with an ambiguity warning on stderr; the implementation ignores stderr and can create from the tag when the caller meant the branch.
  • [writ] Byte-safe registration for harness-owned worktrees #140 — newline-safe registration parsing (P2). Line-oriented parsing of git worktree list --porcelain corrupts valid POSIX paths containing newlines. Needs the documented NUL-delimited --porcelain -z contract. The hook path reads registrations it did not write, so this matters more now.
  • [Hook] Verified worktree reuse so hooks can reattach a reclaimed branch #141 — verified worktree reuse (P2). reject_unproven_resume hard-rejects every pre-existing branch, so there is no supported create → cleanup → reclaim path. Claude Code reuses worktrees by name; this is precisely why the production loop bypasses wh entirely. This is the highest-priority child — Phase 1 does not function without it.

Additional Phase 1 fixes

  • identity.rs is a private mod (lib.rs:9) with 0 tests and holds the strongest code in the repo — the leading_hex_oid_prefix abbreviated-OID smuggling defense (identity.rs:85-153). Make it pub, test it.
  • worktree.rs:271 prune has no sandbox check — it accepts any --repo path.
  • worktree.rs:210-213 list returns repo_root: PathBuf::new() and start_commit: None — listings carry no identity, so nothing downstream can verify a listed worktree. The hook path needs both populated.
  • supervisor.rs --max-parallel is dead in the CLI path (documented at main.rs:120-124: "in this process… does not coordinate across separate wh processes"). Back it with the lease store or drop the flag.

Operating rules (retained — these worked)

  • WIP limit: at most two active implementation branches.
  • Native gates are canonical: repository tests, formatting, linting, Clippy, and builds appropriate to the changed surface.
  • External analyzers are advisory until reproduced. A bot commenting does not create a blocker.
  • No issue per bot comment. Deduplicate by root cause; create a child only for an independently deliverable contract.
  • Prefer deletion and pure decision functions over new flags and duplicated policy prose.

Exit criteria

  • [Hook] Verified worktree reuse so hooks can reattach a reclaimed branch #141 lands: create → cleanup → reclaim works at the same exact start commit, with foreign/moved/adopted branches still fail-closed.
  • [Enforce] Reject ambiguous unqualified start-point refs #139 and [writ] Byte-safe registration for harness-owned worktrees #140 land without weakening exact-base or ownership safety.
  • wh hook handles PreToolUse, WorktreeCreate, WorktreeRemove, SubagentStart, SubagentStop.
  • wh install writes a working hook block into .claude/settings.json.
  • SQLite lease store exists; a WorktreeCreate row is verifiable by direct query, and WorktreeRemove releases it.
  • identity.rs is public and covered by tests.
  • End-to-end in a live Claude Code session: git push --force, git merge, gh pr merge, gh api all blocked with the reason visible to the model.
  • A competing hook's permissionDecision: "allow" cannot override the exit-2 block.
  • False-positive burn-in complete: a normal working session blocks no legitimate command.
  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace pass on the resulting main.
  • Crate names, env vars, and python/ are untouched. Any rename or deletion in this phase is out of scope.

Caution

wh install writes to .claude/settings.json, which is user-owned config outside the repo. Burn-in runs against a throwaway repo with a scoped settings file before anything touches a live multi-repo working configuration.

Parent: #1
Children: #139, #140, #141, #127, #134, #136
Kernel contract: #22, #81 · Classifier: #107 · Schema reservation: #167
Superseded: the #108 graph epic and its children, closed NOT_PLANNED 2026-09-05.

Rewritten by Claude Opus 5, 2026-09-05.

Activity

  1. self-assigned this
    on Aug 30, 2026
  2. linear-code commented on Aug 30, 2026

    @linear-code
    Contributor
  3. 81 remaining items

  4. changed the title [-]Epic: Phase 1 — hook-based enforcement on current crate names[/-] [+]Epic: Phase 1 — Claude Code hook-based enforcement on writ[/+] on Sep 13, 2026
  5. rmems commented on Sep 15, 2026

    @rmems
    OwnerAuthor

    Phase 1 hook slice is in propose-only PR #171 at 47e3202ad6f546aef1208af5873b4335bfe67447.

    writ hook + writ install + SQLite lease skeleton landed. #141/#139/#140 are included. Budget columns reserved, not enforced (#167). Live Claude Code e2e was not available in the implementation VM.

    Cited by: Writ Kernel Steward (Grok Bot)
    Agent: Cursor Grok 4.6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions