---
paths:
  - "src/gzkit/lock_manager.py"
  - "src/gzkit/commands/obpi_lock.py"
  - ".gzkit/handoffs/**"
  - "scripts/session_orientation.py"
---
<!-- Generated by gz agent sync — do not edit -->

<!-- Generated by OBPI-0.0.41-01 — do not edit -->
<!-- rule-version: 0.1.1 -->

# Token-Block Discipline (gzkit)

> **Rule version:** `0.1.1` — trims railway-history pedagogy from the hot-path rule while preserving all lock-release and handoff invariants.

## Doctrine Foundation

**A token cannot be surrendered without a register entry.** Memory is not evidence; lock release is coupled to a handoff document or an explicit abandoned/reaped handoff path.

> See [`docs/governance/token-block-doctrine.md`](../../docs/governance/token-block-doctrine.md) for railway-history lineage, vocabulary mapping, and rationale.

## Binding Sub-Invariant 1: Auditable Abandon Categories

**Rule:** `obpi_lock_release_cmd` and `lock_manager.release_lock()` MUST accept an optional `--abandon <category>:<reason>` flag. When provided, the flag records a degenerate handoff with `abandoned: true` frontmatter and the category+reason text. When omitted, release requires an existing register entry (§ Binding Sub-Invariant 5).

**Category Enum (Base):** The base enum of abandon reasons is CLOSED. New categories require ADR-backed rationale and a checklist item in the next maintenance ADR. Current base:

- `network_loss` — session network interruption; agent unable to gracefully suspend and create register entry
- `external_blocker` — external service/dependency failure blocking normal completion; agent choosing to abandon rather than retry
- `wrong_obpi_claimed` — agent claimed a lock intended for a different OBPI; discovered in release window
- `tool_failure` — gzkit toolchain failure (crash, corruption, unrecoverable state); operator intervention required

**Extension Protocol:** `--abandon <category>:<reason>` where category MUST be in the base enum above or a registered extension (by name in a published ADR). Unregistered categories are rejected (fail-closed) at CLI parsing time. Reason text is free-form within the chosen category.

**Anti-pattern:** Free-text-only abandonment (e.g., `--abandon "session timeout"`) is prohibited. Every abandon must declare a category to enable audit-driven categorization of failure modes.

## Binding Sub-Invariant 2: Register-Entry Minimum-Information Rule

**Rule:** A register entry (handoff document) is structurally valid only if it contains, in frontmatter or body, ALL of the following:

1. **Last lock-event timestamp** — the timestamp of the matching `obpi_lock_claimed` event (from ledger replay) that this handoff concludes. Enables detector to verify: "this handoff matches this lock claim, no lock-release event exists between them."

2. **Last commit SHA** — the HEAD commit SHA at the time of handoff creation. Enables detector to trace work context back to git state.

3. **Named decision context** — a one-paragraph prose statement of the work decision(s) made during this traversal: "decided to defer X," "blocked by Y," "completed Z," "transferred to agent-Q." Not a transcript; a semantic summary of the traversal's purpose and outcome.

4. **Branch state** — the branch name and status (e.g., "main, 3 commits ahead of origin/main", or "feature/foo, diverged at commit abc123"). Enables detector to verify: "handoff-time branch state matches session-start branch state or explains drift."

**Validator Rule (OBPI-0.0.41-04):** Entries lacking any of these four fields MUST fail the `gz validate --lock-handoff-coupling` validator (exit 3, fail-closed). Structurally-valid-but-semantically-empty handoffs (e.g., `title: "Handoff"` + no content) are blocked.

> See [`docs/governance/token-block-doctrine.md`](../../docs/governance/token-block-doctrine.md) for the register-entry rationale.

## Binding Sub-Invariant 3: Reaping Register-Entry Rule

**Rule:** When `lock_manager.reap_expired_locks()` is invoked (by any agent at the start of a new session), a lock found to be expired triggers this sequence:

1. Agent B discovers lock held by Agent A, last claim-event timestamp > TTL-ago (per § Binding Sub-Invariant 4).
2. Agent B MUST create a degenerate handoff with:
   - Frontmatter: `abandoned: true`, `category: reaping`, `abandoned_by: <agent-id>`, `abandoned_at: <ISO-timestamp>`, `previous_agent: <agent-id-from-claim-event>`
   - Body: statement of the reason for reap (e.g., "TTL 24h exceeded; last activity 2026-05-09 14:22:00Z")
   - Last known state snapshot: commit SHA and branch state from the claim event, if available
3. Agent B invokes lock-release via `obpi_lock_release_cmd --abandon reaping:<reason>` with the degenerate handoff in place. Release succeeds only if the degenerate handoff exists.
4. Ledger records `obpi_lock_released_event` with `handoff_path` pointing to the `abandoned_by_reaper` register entry.

**Anti-pattern:** Reaping without a register entry (i.e., claiming reaping is "automatic" and doesn't require handoff) violates the token-block invariant. Every lock surrender — ordinary or reaping — must be paired with a register entry.

## Binding Sub-Invariant 4: TTL Canon and Reaping Discipline

**Rule:** Lock expiration and reaping are time-bound with the following parameters:

### Default TTL Value

- **Default TTL:** 24 hours (86400 seconds)

### Escalation Policy (Warn-Then-Reap)

- **Warn threshold:** At 12 hours (50% TTL), the SessionStart hook logs a WARNING to console and ledger: "OBPI lock held by <agent> for >12h; consider `gz-session-handoff` skill to create register entry and release."
- **Reap threshold:** At 24 hours (100% TTL), the next SessionStart hook invokes `lock_manager.reap_expired_locks()` automatically, records the `abandoned_by_reaper` register entry, and emits the reaping ledger event.
- **Post-reap holdover:** A reaped lock can be re-claimed by the same or a different agent. The ledger trail (abandoned_by_reaper + new claim) is the audit evidence.

### Reaping Authorization Rule

- **Who may reap:** Any agent at the start of a new session (SessionStart hook) has implicit authorization to reap expired locks.
- **Operator override:** An operator may invoke `lock_manager.reap_expired_locks(force=True)` explicitly (via `gz` CLI or script) to reap locks before TTL expiration. Requires explicit `--force` flag and triggers the degenerate-handoff + release sequence immediately.

### Reaping-Attestation Requirement (Mirror of Release Rule)

**Critical:** The reaping agent MUST produce the `abandoned_by_reaper` degenerate register entry BEFORE `lock_manager.release_lock()` succeeds. The release call fails-closed if the handoff does not exist. This mirrors the § Binding Sub-Invariant 5 rule for ordinary release: token surrender always requires a register entry. Reaping is not an exception; it is the same rule applied to degenerate circumstances.

**Attestation Language:** When OBPI-0.0.41-03 implements this rule, the reaping agent's ledger entry MUST include: `"Reaped lock for OBPI-<id> (agent: <A>, TTL exceeded at <timestamp>); created abandoned_by_reaper register entry at <path> before release."` The register entry path is proof the handoff was created; the ledger entry is the agent's attestation that it was created before the reap.

## Binding Sub-Invariant 5: Release Fail-Closed Precondition

**Rule:** `obpi_lock_release_cmd` and `lock_manager.release_lock()` refuse to release a lock UNLESS one of the following is true:

1. A handoff document exists on-disk whose:
   - Frontmatter timestamp (creation time) is later than the matching `obpi_lock_claimed` event timestamp, AND
   - Frontmatter does NOT contain `abandoned: true`, AND
   - Document contains all four minimum-information fields (§ Binding Sub-Invariant 2)

2. OR the caller provides `--abandon <category>:<reason>` flag, which triggers creation of a degenerate handoff (§ Binding Sub-Invariant 1 and 3) before release.

**Staging:** OBPI-0.0.41-02 implements this as a warning-only precondition ("you're releasing without a register entry; this will become fail-closed in 0.0.41-03"). OBPI-0.0.41-03 flips to fail-closed enforcement.

**Validator Enforcement (OBPI-0.0.41-04):** The `gz validate --lock-handoff-coupling` validator replays the ledger, checks every `obpi_lock_released` event, and fails-closes if any event lacks a valid `handoff_path` reference or the referenced handoff violates the minimum-information rule.

## Vocabulary

- **Token** — the OBPI lock artifact (file-based mutex stored in `.gzkit/locks/`)
- **Issue** — the act of claiming a token; corresponds to `obpi_lock_claimed` ledger event
- **Register entry** — the handoff document written to `.gzkit/handoffs/` (canonical storage per OBPI-0.0.41-03); the audit pairing for every lock release
- **Traversal** — a single OBPI work session; may span multiple agent invocations and clock boundaries
- **Abandonment** — surrender of a token due to external circumstance (network loss, blocker, wrong claim, tool failure) recorded via degenerate handoff with `abandoned: true`
- **Reaping** — forcible surrender of an expired token by a different agent (or the same agent in a later session), paired with a degenerate `abandoned_by_reaper` register entry

## Cross-Links

- **AGENTS.md § Behavior Rules:** Behavior Rule #11 (course correction) and Anti-vibing mantra (5:1 governance ratio is the product) establish the governance context for why every token surrender requires explicit verification (the register entry), not memory or good faith.
- **docs/governance/state-doctrine.md § Layer-2 Audit-Coupling:** The register entry is a Layer-2 (ledger) fact; the structural enforcement in § Binding Sub-Invariant 5 ensures every layer-1 lock-release event has a paired layer-2 artifact with full metadata chain.
- **docs/governance/token-block-doctrine.md:** Railway-historical mapping, Tyer's patent details, railway disaster lessons, and the intellectual debt acknowledgement.
- **ADR-0.0.41-token-block-lock-discipline:** Parent ADR; defines the decision, consequences, and OBPI decomposition.

## Audit Path

When troubleshooting lock/handoff coupling failures:

1. `uv run gz validate --lock-handoff-coupling` — fail-closed validator that replays ledger and checks every `obpi_lock_released` event.
2. `grep "obpi_lock_released" .gzkit/ledger.jsonl` — find all release events; each MUST have a `handoff_path` field.
3. `test -f "<path-from-handoff_path>"` — verify register entry exists on-disk.
4. `head -n 10 <path>` — check frontmatter for timestamp, commit SHA, decision context, branch state.
5. Cross-check frontmatter timestamp against `obpi_lock_claimed` timestamp for the same OBPI/agent pair — handoff timestamp must be later.
