brainclaw 1.26.2 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +13 -0
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-coordination.js +65 -1
  4. package/dist/commands/attempt-authority.js +80 -0
  5. package/dist/commands/harvest.js +140 -61
  6. package/dist/commands/loop.js +34 -0
  7. package/dist/commands/loops-handlers.js +143 -15
  8. package/dist/commands/mcp-catalog.js +52 -18
  9. package/dist/commands/mcp-schemas.generated.js +64 -0
  10. package/dist/commands/mcp-write-claims.js +128 -1
  11. package/dist/commands/mcp-write-coordination.js +149 -76
  12. package/dist/core/agent-capability.js +1 -1
  13. package/dist/core/agentrun-reconciler.js +148 -22
  14. package/dist/core/agentruns.js +254 -29
  15. package/dist/core/assignment-request-schema.js +7 -0
  16. package/dist/core/assignment-sweeper.js +5 -3
  17. package/dist/core/assignments.js +131 -33
  18. package/dist/core/claim-request-schema.js +7 -0
  19. package/dist/core/claims.js +53 -2
  20. package/dist/core/dispatch-status.js +16 -6
  21. package/dist/core/dispatcher.js +51 -51
  22. package/dist/core/entity-operations.js +20 -0
  23. package/dist/core/events.js +4 -0
  24. package/dist/core/execution-adapters.js +189 -14
  25. package/dist/core/execution-contract.js +345 -0
  26. package/dist/core/execution.js +130 -16
  27. package/dist/core/facade-schema.js +3 -0
  28. package/dist/core/harness-adapters/base.js +150 -0
  29. package/dist/core/harness-adapters/claude.js +39 -0
  30. package/dist/core/harness-adapters/codex.js +57 -0
  31. package/dist/core/harness-adapters/harvest.js +109 -0
  32. package/dist/core/harness-adapters/index.js +8 -0
  33. package/dist/core/harness-adapters/prompt-only.js +13 -0
  34. package/dist/core/harness-adapters/registry.js +48 -0
  35. package/dist/core/harness-adapters/result.js +33 -0
  36. package/dist/core/harness-adapters/types.js +2 -0
  37. package/dist/core/ideation-loop-close.js +25 -2
  38. package/dist/core/instruction-templates.js +3 -2
  39. package/dist/core/loop-turn-dispatch.js +235 -0
  40. package/dist/core/loops/artifact-contract.js +11 -0
  41. package/dist/core/loops/attempt-authority.js +496 -0
  42. package/dist/core/loops/attempt-generations.js +509 -0
  43. package/dist/core/loops/attempt-reservation.js +197 -35
  44. package/dist/core/loops/attempt-rollout.js +404 -0
  45. package/dist/core/loops/attempt-takeover.js +155 -0
  46. package/dist/core/loops/bootstrap-acquire.js +7 -3
  47. package/dist/core/loops/brief-assembly.js +21 -4
  48. package/dist/core/loops/evidence.js +188 -0
  49. package/dist/core/loops/facade-schema.js +75 -11
  50. package/dist/core/loops/gate-policy.js +533 -0
  51. package/dist/core/loops/impl-bind.js +91 -81
  52. package/dist/core/loops/index.js +9 -0
  53. package/dist/core/loops/iteration-engine.js +31 -19
  54. package/dist/core/loops/kind-policies.js +90 -0
  55. package/dist/core/loops/lock.js +71 -13
  56. package/dist/core/loops/reconcile-turn.js +237 -18
  57. package/dist/core/loops/result-reducers.js +113 -10
  58. package/dist/core/loops/store.js +34 -3
  59. package/dist/core/loops/turn-execution.js +480 -0
  60. package/dist/core/loops/types.js +127 -3
  61. package/dist/core/loops/verbs.js +335 -99
  62. package/dist/core/loops/verify-command.js +105 -20
  63. package/dist/core/loops/workspace-digest.js +54 -0
  64. package/dist/core/review-loop-close.js +25 -3
  65. package/dist/core/review-loop-turn-dispatch.js +210 -161
  66. package/dist/core/runtime-signals.js +62 -25
  67. package/dist/core/schema.js +40 -0
  68. package/dist/core/spawn-check.js +3 -2
  69. package/dist/core/upgrades/backup.js +27 -4
  70. package/dist/facts.js +9 -8
  71. package/dist/facts.json +8 -7
  72. package/docs/cli.md +49 -1
  73. package/docs/concepts/attempt-authority.md +407 -0
  74. package/docs/concepts/evidence-attestations.md +135 -0
  75. package/docs/concepts/execution-contract.md +166 -0
  76. package/docs/concepts/harness-adapters.md +166 -0
  77. package/docs/concepts/ideation-loop.md +5 -4
  78. package/docs/concepts/loop-engine.md +302 -113
  79. package/docs/index.md +4 -1
  80. package/docs/integrations/codex.md +3 -3
  81. package/docs/integrations/mcp.md +59 -5
  82. package/docs/loops/debug.md +144 -0
  83. package/docs/loops/ideation.md +158 -0
  84. package/docs/loops/implementation.md +174 -0
  85. package/docs/loops/research.md +136 -0
  86. package/docs/loops/review.md +200 -0
  87. package/docs/mcp-schema-changelog.md +18 -5
  88. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  # Loop engine
2
2
 
3
- brainclaw coordinates many agents against shared state.
4
- The Loop engine turns repetitive multi-turn workflows
5
- review, ideation, implementation, research, and debugging —
6
- into **first-class, persistable, automatable objects**.
3
+ brainclaw coordinates many agents against shared state. The Loop engine
4
+ turns repetitive multi-turn workflows — review, ideation, implementation,
5
+ research, and debug into **first-class, persistable, automatable objects**
6
+ sharing one common runtime.
7
7
 
8
8
  Status: **shipped**. `bclaw_loop` exposes the persistent engine and its five
9
9
  built-in protocols; `bclaw_coordinate` and `bclaw_dispatch` add ergonomic
@@ -11,6 +11,30 @@ shortcuts for ideation and review. This document retains the RFC-level
11
11
  concurrency contract and implementation history where it explains an
12
12
  invariant, but its operational sections describe the surface available today.
13
13
 
14
+ **How this doc is organised.** It starts with the shared engine — authority,
15
+ phases, artifacts, lifecycle verbs, gates, recovery, observability — and
16
+ then points at five per-protocol guides. Each protocol has its own page in
17
+ [`docs/loops/`](../loops/); this page is not a review guide. The five
18
+ protocols are equal citizens of the engine, and this is not a review feature
19
+ with a few extensions.
20
+
21
+ - [Review](../loops/review.md) — validate a change against a reviewer.
22
+ - [Ideation](../loops/ideation.md) — pressure-test a proposal against
23
+ project memory.
24
+ - [Implementation](../loops/implementation.md) — drive a plan+sequence to a
25
+ green verify.
26
+ - [Research](../loops/research.md) — converge open-ended investigation to
27
+ a synthesis.
28
+ - [Debug](../loops/debug.md) — drive a broken system back to green.
29
+
30
+ Identity, dispatch decisions and spawn authority live in a separate façade
31
+ over the reservation core — see
32
+ [Attempt authority](./attempt-authority.md). The exact worker, workspace,
33
+ artifact and evidence expectations for each physical turn are frozen by the
34
+ [Execution contract and capability snapshot](./execution-contract.md).
35
+ Artifacts then cross the server-controlled commit boundary described in
36
+ [Evidence envelopes, attestations, and protocol gates](./evidence-attestations.md).
37
+
14
38
  ## Why
15
39
 
16
40
  Without a loop, recurring work is easy to reduce to manual ping-pong:
@@ -38,6 +62,72 @@ A Loop is a **persistent thread of structured work** with:
38
62
  A Loop stores *references* to existing objects — it never duplicates them.
39
63
  Claims, handoffs, and candidates remain the source of truth for their own data.
40
64
 
65
+ ## Attempt authority
66
+
67
+ Every dispatched turn crosses several boundaries — the loop mints identity,
68
+ an assignment must be persisted, a run must be launched, evidence must
69
+ eventually be accepted. `AttemptAuthority` owns those execution decisions while
70
+ the Loop Engine owns phases, artifacts, gates, and convergence. It is common to
71
+ all five kinds and adds no event journal.
72
+
73
+ `prepareTurnExecution` applies the same projections-before-crossing path to
74
+ every worker phase and refuses `engine` or `manual` phases before reservation.
75
+ The first worker phase for a `(loop_id, slot_id, iteration)` keeps the legacy
76
+ deterministic `turn_id`. If that same reusable slot enters another worker phase
77
+ without an iteration bump, the resolver derives a versioned identity from
78
+ `(loop_id, slot_id, phase, iteration)` instead. Replays of the same phase still
79
+ adopt one cell, while compatible legacy reservations remain adoptable during
80
+ crash recovery and upgrades. This rule is kind-neutral: it prevents one phase
81
+ from inheriting another phase's already-consumed launch authority in review,
82
+ ideation, research, debug, or any future multi-phase protocol.
83
+ The first physical generation follows `reserve → commit → durable projections
84
+ → launch(0)`. A fenced takeover keeps the same `turn_id` and `assignment_id`,
85
+ but creates a new epoch, run, nonce, contract, and isolated workspace. Re-entry
86
+ through the same common path projects that successor and races
87
+ `launch(next_epoch)` immediately before spawn.
88
+
89
+ Completion is accepted only on the full generation fence. Settlement and
90
+ takeover contend on one immutable `close(epoch)` decision, so an old worker
91
+ cannot settle after a successor wins. Mutable AgentRun and head records are
92
+ replayable projections, not authority. See [Attempt authority](./attempt-authority.md)
93
+ for the Windows-safe publish protocol, two-release activation, and recovery.
94
+
95
+ Before reservation, that common adapter resolves the selected agent against a
96
+ typed capability requirement and hashes an immutable ExecutionContract. The
97
+ full contract lives on TurnReservation; Assignment and AgentRun carry the same
98
+ hash/reference and capability snapshot before crossing. This is one shared
99
+ dispatch substrate for all five protocols, not protocol-specific review
100
+ metadata. See
101
+ [Execution contract and capability snapshot](./execution-contract.md).
102
+
103
+ The contracted attempt then passes through a
104
+ [Harness adapter](./harness-adapters.md). That adapter binds a concrete agent
105
+ harness and normalizes its output, while the existing `ExecutionAdapter` owns
106
+ the process transport. Neither layer owns phases, artifacts, evidence, gates,
107
+ or convergence: those remain here in the shared engine. The same boundary is
108
+ used for every worker phase in the five-kind table below.
109
+
110
+ Phase-specific execution metadata lives in `LOOP_KIND_POLICIES`; phase graphs,
111
+ gates, iteration and stop conditions remain exclusively in `DEFAULT_PROTOCOLS`.
112
+ The current execution split is deliberately visible here for all five protocols:
113
+
114
+ | Kind | Worker phases | Engine phases | Manual phases | Integration required before convergence |
115
+ |---|---|---|---|---|
116
+ | `review` | `findings`, `author_response`, `followup_review` | `verdict` | `change_summary` | `author_response` |
117
+ | `ideation` | `critique`, `revision`, `synthesis` | — | `proposal` | — |
118
+ | `implementation` | `execute` | `bind`, `verify` | `handoff_ready` | `execute` |
119
+ | `research` | `investigate`, `synthesize` | `conclude` | — | — |
120
+ | `debug` | `reproduce`, `hypothesize`, `isolate`, `fix` | — | `handoff` | `fix` |
121
+
122
+ Every worker result must carry the phase's explicit `artifact_type`. A summary
123
+ is useful observability, but it is never proof that opens a gate. Report harvest
124
+ may reconcile read-only phases; mutating phases keep their claim until
125
+ `harvest --integrate` has integrated the worktree.
126
+ The concurrency and recovery contract lives in the dedicated document —
127
+ see [Attempt authority](./attempt-authority.md) for the full model,
128
+ identity matrix, ordered dispatch and invariants I1–I18. This page assumes
129
+ that contract without restating it.
130
+
41
131
  ## Data model
42
132
 
43
133
  ```ts
@@ -64,6 +154,10 @@ interface LoopThread {
64
154
  artifacts: LoopArtifact[];
65
155
  linked?: LoopLinks; // top-level context only (plan/sequence). Other refs live on artifacts/slots.
66
156
  stop_condition?: StopCondition;
157
+ evidence_policy?: { // absent only on explicit/pre-policy threads
158
+ version: 'gate-policy-v1';
159
+ mode: 'shadow' | 'strict';
160
+ };
67
161
 
68
162
  created_at: string; // ISO
69
163
  updated_at: string;
@@ -75,12 +169,19 @@ type LoopStatus = 'open' | 'paused' | 'completed' | 'blocked' | 'cancelled';
75
169
  type ReviewMode = 'asymmetric' | 'symmetric';
76
170
 
77
171
  interface LoopProtocolConfig {
78
- review_mode?: ReviewMode; // review loops persist their selected mode so resume/turn handlers are deterministic
172
+ review_mode?: ReviewMode;
173
+ iteration?: { cycle: string[]; max_iterations: number; exit_when: 'critic_signal' | 'no_new_critique_artifacts' | 'command_green' };
174
+ verify?: { command: string[]; timeout_ms?: number };
175
+ preset?: string;
176
+ max_operator_questions?: number;
177
+ max_pause_duration?: string; // ISO-8601 duration
79
178
  }
80
179
 
81
180
  interface LoopPhase {
82
181
  name: string;
83
182
  advance_when?: 'all' | 'any'; // default 'all' — every slot turn in this phase must be `done` before advance
183
+ context_filter?: LoopContextCategory[];
184
+ advance_gate?: StopCondition;
84
185
  }
85
186
 
86
187
  interface LoopSlot {
@@ -91,7 +192,8 @@ interface LoopSlot {
91
192
  assignment_id?: string; // set when a turn is dispatched
92
193
  claim_id?: string; // for execution loops, the claim held by this slot
93
194
  phase?: string; // which phase this slot currently participates in (supports parallel slots per phase)
94
- status: 'open' | 'assigned' | 'working' | 'done';
195
+ status: 'open' | 'assigned' | 'working' | 'waiting_input' | 'done' | 'failed' | 'cancelled';
196
+ current_turn_id?: string; // immutable attempt currently owning this reusable slot
95
197
  }
96
198
 
97
199
  interface LoopArtifact {
@@ -100,8 +202,9 @@ interface LoopArtifact {
100
202
  type: string; // "finding" | "synthesis" | "verdict" | "plan_draft" | ...
101
203
  ref?: LoopRef; // preferred: link to an existing primitive
102
204
  body?: string; // inline content ≤ 4 KB; else force `ref`
103
- produced_by?: SlotId;
205
+ produced_by?: string; // derived server-side from slot/engine/coordinator context
104
206
  produced_at: string;
207
+ evidence?: EvidenceEnvelope; // server-sealed; never caller-authored
105
208
  }
106
209
 
107
210
  type LoopRef =
@@ -123,7 +226,10 @@ type AtomicStopCondition =
123
226
  | { kind: 'phase_reached'; phase: string }
124
227
  | { kind: 'reviewer_green' } // an `accepted` verdict artifact in any phase
125
228
  | { kind: 'max_iterations'; n: number } // hard cap; on hit, close with status=blocked
229
+ | { kind: 'min_iterations'; n: number }
126
230
  | { kind: 'artifact_produced'; phase: string; type: string }
231
+ | { kind: 'min_artifacts_by_type'; type: string; n: number; scope: 'phase' | 'loop' }
232
+ | { kind: 'no_open_questions' }
127
233
  | { kind: 'manual' }; // only closes on explicit close
128
234
 
129
235
  type StopCondition =
@@ -132,6 +238,9 @@ type StopCondition =
132
238
  | { kind: 'all'; conditions: StopCondition[] }; // AND — every clause must match
133
239
 
134
240
  // LoopEvent is a discriminated union with typed per-kind payloads (no loose `payload` map).
241
+ // The excerpt below shows the base lifecycle. The shipped union also includes
242
+ // turn_reserved, phase_advance_blocked, max_iterations_reached, input/file-apply,
243
+ // and slot-status events. Gate-driving transitions carry GateDecision.
135
244
  interface LoopEventBase {
136
245
  event_id: string; // ULID
137
246
  loop_id: LoopId;
@@ -146,6 +255,7 @@ type LoopEvent =
146
255
  | (LoopEventBase & { kind: 'phase_advanced'; from_phase: string; to_phase: string; iteration: number; reason?: string })
147
256
  | (LoopEventBase & { kind: 'turn_assigned'; slot_id: SlotId; phase: string; assignment_id?: string; input?: string; retry_of?: string /* prior event_id */ })
148
257
  | (LoopEventBase & { kind: 'turn_completed'; slot_id: SlotId; phase: string; artifact_id?: string; outcome: 'done' | 'failed' | 'cancelled'; failure_reason?: string })
258
+ | (LoopEventBase & { kind: 'attempt_generation_changed'; slot_id: SlotId; turn_id: string; assignment_id: string; from_epoch: number; to_epoch: number; from_run_id: string; to_run_id: string; close_digest: string; cause: string })
149
259
  | (LoopEventBase & { kind: 'artifact_added'; artifact_id: string; phase: string; type: string; produced_by?: SlotId })
150
260
  | (LoopEventBase & { kind: 'linked'; target: LoopRef })
151
261
  | (LoopEventBase & { kind: 'paused'; reason?: string })
@@ -223,18 +333,43 @@ file lists which artifact types are ref-based and which use inline JSON bodies.
223
333
 
224
334
  ## Lifecycle verbs
225
335
 
226
- The engine exposes four active verbs. Each one mutates state, appends an event, and returns the updated `LoopThread`. **All verbs are strictly synchronous-on-state and asynchronous-on-work**: any downstream dispatch (spawning a CLI, calling another MCP tool) is fire-and-forget from the commit window, so the per-loop lock is always released quickly.
336
+ The engine exposes one shared lifecycle for every protocol. Mutating verbs
337
+ persist state plus causal events and return the updated `LoopThread`.
338
+ **All verbs are strictly synchronous-on-state and asynchronous-on-work**: any
339
+ downstream dispatch continues outside the commit window so the per-loop lock
340
+ is released quickly.
227
341
 
228
342
  - **open** — create a new loop. Inserts `opened` event; `current_phase` set to `phases[0].name`.
229
- - **turn** — record that a phase's work is assigned to a slot. Fire-and-forget dispatch: the handler kicks off the downstream call (e.g. `bclaw_coordinate` to spawn a CLI) and returns immediately. `slot.status` flips to `'assigned'` with an `assignment_id`; the actual work continues outside the lock. Inserts `turn_assigned`. The slot reports back later via a separate `complete_turn` call.
343
+ - **turn** — record that a phase's work is assigned to a slot. With
344
+ `dispatch` absent/false it performs state mutation only and may receive an
345
+ existing `assignment_id`. Trusted `dispatch: true` routes a worker phase
346
+ through the common AttemptAuthority preparation/projection/crossing path and
347
+ launches outside the loop lock; engine/manual phases are refused before
348
+ reservation. `slot.status` flips to `'assigned'`, and the worker reports back
349
+ later through harvest/reconciliation or `complete_turn`. Inserts
350
+ `turn_assigned`.
230
351
  - **advance** — evaluate `stop_condition`; if satisfied, `close` with `status=completed`. Otherwise, transition `current_phase` to the next phase (or a specified one). Inserts `phase_advanced`. If `advance` revisits an earlier phase (e.g. a fixup round re-enters `findings`), `iteration_count` increments.
231
352
  - **close** — terminal: set `status` to `completed | cancelled | blocked` and `closed_at`. Inserts `closed`.
232
353
 
233
- Two auxiliary verbs cover quality of life:
354
+ Additional shared and engine-owned actions complete the lifecycle:
234
355
 
235
356
  - **pause** / **resume** — suspend a loop without closing (e.g. waiting on an external input).
236
357
  - **add_artifact** — attach an artifact to a phase without moving on.
237
358
  - **complete_turn** — close out a previously-assigned turn: flips `slot.status` to `'done'` (or `'failed' | 'cancelled'`), optionally attaches an artifact carrying the outcome. Emitted by the slot agent itself when its dispatched work returns. Separate from `turn` precisely because the dispatch is async. Authorization is strict: the caller's `agentId` must equal that slot's `agent_id`, unless the caller is the loop's `created_by`, which is the only admin override.
359
+ - **takeover** — coordinator-only cross-kind recovery action. It closes the
360
+ active physical generation, arms a successor in a distinct isolated
361
+ workspace, and records `attempt_generation_changed`. It does not spawn; the
362
+ normal turn dispatch must still win the successor's launch cell.
363
+ - **request_input** / **provide_input** — bounded, evidence-backed operator clarification usable by any protocol.
364
+ - **bind** — implementation-loop engine action that validates the linked sequence and advances to `execute`; it never launches a worker.
365
+ - **verify** — implementation/debug engine action that runs the opener-configured command outside the loop lock, then records a verification-attested report.
366
+
367
+ Artifact authority is sealed at these verb boundaries. `produced_by` is
368
+ derived from the authenticated slot/engine/coordinator context. A narrative
369
+ `accepted` verdict or `{passed:true}` report is stored but cannot open a gate
370
+ unless its envelope carries the policy-specific approval or verification
371
+ attestation. Gate decisions and rejection reasons are persisted on causal
372
+ LoopEvents; RuntimeEvents remain telemetry only.
238
373
 
239
374
  ## MCP facade: `bclaw_loop(intent)`
240
375
 
@@ -250,16 +385,21 @@ interface BclawLoopCallerEnvelope {
250
385
 
251
386
  // Per-intent payloads. Every mutating intent supports `expected_version` + `client_request_id`.
252
387
  type BclawLoopInput = BclawLoopCallerEnvelope & (
253
- | { intent: 'open'; kind: LoopKind; title: string; goal?: string; phases?: LoopPhase[]; slots?: Partial<LoopSlot>[]; linked?: LoopLinks; stop_condition?: StopCondition; mode?: ReviewMode /* review only; persisted to loop.protocol.review_mode; default 'asymmetric' */ }
254
- | { intent: 'turn'; loop_id: LoopId; slot_id?: SlotId; role?: string; input?: string; dispatch?: boolean; expected_version?: number }
255
- | { intent: 'complete_turn'; loop_id: LoopId; slot_id: SlotId; artifact?: Omit<LoopArtifact, 'artifact_id' | 'produced_at'>; outcome?: 'done' | 'failed' | 'cancelled'; failure_reason?: string; expected_version?: number }
388
+ | { intent: 'open'; kind: LoopKind; title: string; goal?: string; phases?: LoopPhase[]; slots?: Partial<LoopSlot>[]; linked?: LoopLinks; stop_condition?: StopCondition; mode?: ReviewMode /* review only */; verify?: { command: string[]; timeout_ms?: number }; allow_orphan?: boolean }
389
+ | { intent: 'turn'; loop_id: LoopId; slot_id?: SlotId; role?: string; input?: string; assignment_id?: string; claim_id?: string; dispatch?: boolean; auto_execute?: boolean; model?: string; target_agents?: string[]; expected_version?: number }
390
+ | { intent: 'complete_turn'; loop_id: LoopId; slot_id: SlotId; assignment_id?: string; turn_id?: string; run_id?: string; nonce?: string; attempt_epoch?: number; execution_contract_hash?: string; workspace_digest?: string; artifact?: Pick<LoopArtifact, 'phase' | 'type' | 'body' | 'ref' | 'addresses_critique'>; outcome?: 'done' | 'failed' | 'cancelled'; failure_reason?: string; expected_version?: number }
391
+ | { intent: 'takeover'; loop_id: LoopId; slot_id: SlotId; turn_id: string; expected_epoch: number; cause: string; liveness_evidence: string; external_effect_policy: 'none' | 'idempotent' | 'externally_fenced'; next_workspace_path: string; takeover_mode?: 'takeover' | 'retry' }
256
392
  | { intent: 'advance'; loop_id: LoopId; to_phase?: string; reason?: string; force?: boolean; expected_version?: number }
257
- | { intent: 'add_artifact'; loop_id: LoopId; artifact: Omit<LoopArtifact, 'artifact_id' | 'produced_at'>; expected_version?: number }
393
+ | { intent: 'add_artifact'; loop_id: LoopId; artifact: Pick<LoopArtifact, 'phase' | 'type' | 'body' | 'ref' | 'addresses_critique'>; expected_version?: number }
258
394
  | { intent: 'pause'; loop_id: LoopId; reason?: string; expected_version?: number }
259
395
  | { intent: 'resume'; loop_id: LoopId; expected_version?: number }
260
396
  | { intent: 'close'; loop_id: LoopId; status: 'completed' | 'cancelled' | 'blocked'; reason?: string; expected_version?: number }
397
+ | { intent: 'verify'; loop_id: LoopId }
398
+ | { intent: 'bind'; loop_id: LoopId; dry_run?: boolean; lanes?: string[]; auto_execute?: boolean; model?: string; max_assignments?: number }
399
+ | { intent: 'request_input'; loop_id: LoopId; slot_id: SlotId; phase: string; question_text: string; evidence: string[]; suggested_default?: string; options?: OperatorQuestionOption[]; pause_scope: 'slot' | 'loop'; on_timeout: 'use_default' | 'cancel_loop' | 'continue_incomplete'; timeout_at?: string; expected_version?: number }
400
+ | { intent: 'provide_input'; loop_id: LoopId; replies_to: string; resolved_via: 'answer' | 'choose' | 'skip' | 'timeout_default'; answer_text?: string; chosen_option_id?: string; by?: 'operator' | 'system'; expected_version?: number }
261
401
  | { intent: 'get'; loop_id: LoopId; include_events?: boolean }
262
- | { intent: 'list'; kind?: LoopKind; status?: LoopStatus; linked_plan_id?: string; limit?: number; offset?: number }
402
+ | { intent: 'list'; kind?: LoopKind; status?: LoopStatus; limit?: number; offset?: number }
263
403
  );
264
404
 
265
405
  // Standard facade envelope, matching bclaw_work / bclaw_coordinate output shape.
@@ -285,6 +425,18 @@ type NextExpectedHint =
285
425
  | { action: 'close'; intent: 'bclaw_loop.close'; reason: string };
286
426
  ```
287
427
 
428
+ The `complete_turn` fence fields remain optional in the transport schema for
429
+ legacy turns. When the slot is backed by AttemptAuthority v2, the runtime
430
+ requires the complete tuple — assignment, turn, run, nonce, epoch, execution
431
+ contract hash and workspace digest — and rejects stale or partial evidence
432
+ before mutating the slot or LoopEvent journal.
433
+
434
+ For `turn(dispatch:true)`, a slot with a frozen agent keeps that identity. An
435
+ unbound slot can instead receive `target_agents`; Brainclaw resolves the
436
+ capability requirement deterministically, independent of array order. A
437
+ pre-cross rejection can therefore exclude that candidate and replay selection
438
+ without minting a second attempt.
439
+
288
440
  **Why a single facade, not `bclaw_loop_open`/`_advance`/`_close` tools.** Consistency beats granularity for agent-facing DX. The two existing facades are intent-based; adding a third in the same style keeps the surface small and predictable. Agents that need low-level control can still go to the underlying store (local file reads, not MCP).
289
441
 
290
442
  **Slot-bound auth.** `complete_turn` is a slot-owned mutation, so the server must resolve the target slot inside the lock and verify `caller.agentId === slot.agent_id`. If not, reject with `unauthorized_slot_write`. The single admin fallback is `caller.agentId === loop.created_by`, which allows the loop owner to recover a wedged slot or cancel it explicitly. Any future slot-specific intent added to this facade inherits the same rule.
@@ -315,8 +467,8 @@ one workflow among the five.
315
467
  will drive or dispatch the resulting loop rather than creating an inert thread.
316
468
  The shared lifecycle verbs are `turn`, `complete_turn`, `advance`,
317
469
  `add_artifact`, `pause`, `resume`, and `close`. Implementation loops additionally
318
- use `bind` to dispatch their linked sequence and `verify` to run their declared
319
- command.
470
+ use engine-only `bind` to validate their linked sequence and enter `execute`,
471
+ then `turn(dispatch:true)` for worker slots; `verify` runs their declared command.
320
472
 
321
473
  ### Clarification is a cross-cutting primitive
322
474
 
@@ -341,78 +493,50 @@ The Loop engine is a **control plane**; existing primitives remain the **data pl
341
493
 
342
494
  A Loop never copies these objects — it links them. Deleting the linked primitive does not break the loop; the reference just becomes dangling, surfaced in diagnostics.
343
495
 
344
- ## Review automation (one workflow)
345
-
346
- Review is the most automated convenience path: manual review round-trips can
347
- disappear. Its special handling below does not change the general Loop Engine
348
- model described in [Supported workflows](#supported-workflows).
349
-
350
- The existing `review` intent in `bclaw_coordinate` already creates a review candidate. We extend it — **strictly backward-compatible** — with an optional flag `open_loop?: boolean` that **defaults to `false`**. Every existing `review` call behaves exactly as today; a caller must explicitly opt in by passing `open_loop: true`. The coordinate enum was extended in v1.5.0 to add `ideate` (memory-confrontation ideation_loop driver — see [ideation-loop.md](./ideation-loop.md) for the full design and §[Automation: extending `bclaw_coordinate(intent='ideate')`](#automation-extending-bclaw_coordinateintentideate) below for a summary). The current vocabulary is `assign | consult | review | reroute | summarize | ideate`. A future minor version may flip the `open_loop` default after telemetry confirms adoption, but such a flip will be gated by MCP schema versioning (pln#392) and surfaced in the changelog.
351
-
352
- When `bclaw_coordinate(intent='review', open_loop: true)` is called, it:
353
-
354
- 1. Creates the review candidate as today.
355
- 2. Opens a `review` loop via `bclaw_loop(intent: 'open', kind: 'review', ...)` with slots `{role: 'author', agent: caller}`, `{role: 'reviewer', agent: target}`.
356
- 3. Links the provided handoff/candidate to the loop as an artifact at `change_summary`.
357
- 4. Advances to `findings` and calls `bclaw_loop(intent: 'turn')` to dispatch to the reviewer.
358
- 5. On turn completion with a verdict artifact, auto-advances; `reviewer_green` stop closes.
359
- 6. On a `request_changes` verdict, the fix cycle re-dispatches the reviewer into the same worktree until `approve` or the `max_iterations` cap.
360
-
361
- **How the verdict reaches the loop (shipped, pln#628 Focus 4B).** A dispatched reviewer worker does not call `bclaw_loop` itself — it writes its outcome to `LANE-RESULT.json` at the worktree root, including an optional `review_verdict` (`approve` | `request_changes`) and `review_summary`. When the coordinator runs `brainclaw harvest <assignment_id>` (report-only path and `--integrate`), a review lane carrying a `review_verdict` is mapped onto its loop: brainclaw records a `verdict` artifact on the reviewer slot (`approve` → an `accepted…` body) and calls `advance`, which **auto-closes the loop on `reviewer_green` for `approve`** — no human driving `complete_turn`/`advance`.
362
-
363
- **The autonomous fix cycle (PR2, `--integrate` only).** On `request_changes`, `harvest --integrate` bumps the loop's round counter, **keeps the claim + worktree alive**, and re-dispatches the same reviewer slot into that **same worktree** (symmetric mode) with a findings-aware brief: apply the requested changes in place, then re-review. Commits accumulate on one branch — no fresh worktree per turn, so the branch-per-scope / refuse-unharvested-commits invariants are never tripped. The cycle repeats until `approve` (→ `reviewer_green` close) or the `max_iterations` cap (n=3 → auto-close `blocked`, handed to a human). The report-only harvest path never cycles (it can neither re-dispatch nor retain the claim); it defers `request_changes` to `--integrate` and still closes on `approve`. The mapping is idempotent, resolves the reviewer slot strictly by `assignment_id` (so symmetric multi-reviewer loops target the right slot), and runs the `complete_turn`+`advance` pair under the loop lock so an interrupted pass resumes rather than stalls. Asymmetric (author ≠ reviewer) cross-agent worktree sharing is a planned follow-up.
364
-
365
- ### Symmetric review-AND-fix mode
366
-
367
- By default, the phases `findings` and `author_response` follow the classical asymmetric split the reviewer identifies issues, the author applies fixes on the next turn. That doubles the number of round-trips: every issue needs one full turn to be identified, then another to be fixed.
368
-
369
- When both slots are coding agents with write access to the artifact under review (the common case with `bclaw_coordinate(intent='review', open_loop: true, mode: 'symmetric')`), the protocol collapses those two roles into one behavior per turn: **the reviewer reviews AND applies whatever fixes it can make directly**, then returns a summary artifact of changes applied + a request for the other slot to review those changes. The other slot then takes its turn with the same semantics — review-and-fix on whatever is left — and so on. Exit is reached when a reviewer turn produces a green verdict with no unapplied findings and with `changes_applied` omitted or empty for that turn, or when `max_iterations` is hit.
370
-
371
- The phase sequence stays the same (`findings → author_response → followup_review`), but each turn may emit at most one `changes_applied` artifact alongside any `finding` artifacts. That artifact must summarize the concrete edits made in that turn and point at the mutated object via `ref` when one exists (candidate, handoff, message, or other linked primitive); it is a turn summary, not a second source of truth. The next-turn handler always starts from the committed-and-reviewed state of the previous turn, not from the original draft. This halves the round-trip count when fixes are mechanical enough for the reviewer to own, which is the common case for spec work and small-to-mid refactors.
372
-
373
- Selector: `mode: 'symmetric' | 'asymmetric'` on the `open_loop` call (or directly on `bclaw_loop(intent='open', kind='review', mode:…)`). Defaults to `asymmetric` for safety. On `open`, the server persists the resolved selection to `loop.protocol.review_mode` so resume/turn handlers do not depend on the original request envelope. If `symmetric` is requested but the active slot is human-operated or lacks write authority to the reviewed artifact, that turn degrades gracefully to asymmetric behavior for that slot: findings/verdicts are still allowed, `changes_applied` is omitted, and the loop proceeds without protocol error. Implementation-loops and security reviews typically stay asymmetric; RFC and doc reviews benefit most from symmetric.
374
-
375
- The operator never copy-pastes. They see status in the board (`bclaw_context(kind="board")`) and can `bclaw_loop(intent="get", loop_id=…)` for detail.
376
-
377
- ## Automation: extending `bclaw_coordinate(intent='ideate')`
378
-
379
- Shipped in v1.5.0 (pln#492). The full design — phases, context_filter,
380
- iteration block, advance_gate, brief assembly, system events,
381
- single-agent vs multi-agent UX — lives in [ideation-loop.md](./ideation-loop.md).
382
- Summary for the loop-engine perspective:
383
-
384
- - `bclaw_coordinate(intent='ideate', task=…, [targetAgents=[…]])` opens
385
- an ideation_loop with the caller as `champion` slot and the targets
386
- (when provided) as `critic` slots. The task is stored verbatim as
387
- the `proposal` artifact (sliced to the 4 KB body cap).
388
- - Single-agent mode (no `targetAgents`): the loop opens at the
389
- proposal phase and stops there. The champion drives the cycle
390
- manually via `bclaw_loop(intent='turn'|'advance')`. Useful when the
391
- loop's structure (memory filter, gate, iteration accounting) is
392
- what's wanted, not the multi-slot orchestration.
393
- - Multi-agent mode (explicit `targetAgents`): the driver advances
394
- proposal → critique and dispatches a turn per critic with a brief
395
- assembled by `buildIdeationBrief` — context-filtered (critic sees
396
- only `traps + feedback + runtime_notes + critique_history`),
397
- BM25-ranked via `search()`, capped at 48 KB.
398
-
399
- The ideation_loop introduces three loop-engine extensions consumed by
400
- this driver:
401
-
402
- - `LoopPhase.context_filter?: LoopContextCategory[]` — closed enum
496
+ ## Per-protocol guides
497
+
498
+ Each of the five kinds has its own operator-facing guide with the same
499
+ template purpose, default protocol, entry points, advance gates, stop
500
+ condition, artifacts, routing, recovery, "when NOT to use", reference
501
+ implementation. Consult them for anything protocol-specific.
502
+
503
+ - [Review](../loops/review.md) — the most automated coordinator shortcut,
504
+ autonomous fix cycle on `request_changes`, symmetric review-and-fix mode.
505
+ - [Ideation](../loops/ideation.md) — memory-confrontation with a
506
+ per-phase context filter and a `critique↔revision` iteration block; see
507
+ also the full RFC in [ideation-loop.md](./ideation-loop.md).
508
+ - [Implementation](../loops/implementation.md) `bind execute↔verify
509
+ handoff_ready`, deterministic `command_green` exit.
510
+ - [Research](../loops/research.md) `investigate↔synthesize conclude`,
511
+ no `blocked` outcome, `critic_signal` exit.
512
+ - [Debug](../loops/debug.md) — `reproduce → hypothesize↔isolate↔fix →
513
+ handoff`, mirrors implementation's `command_green` gate on the repro.
514
+
515
+ `bclaw_coordinate` exposes two of these as ergonomic shortcuts today:
516
+ `intent='review'` (with `open_loop: true`) and `intent='ideate'`. Both
517
+ were extended strictly backward-compatibly — every prior call still
518
+ behaves as before. The current coordinate vocabulary is
519
+ `assign | consult | review | reroute | summarize | ideate`.
520
+
521
+ ## Common engine extensions used by protocols
522
+
523
+ Three engine extensions are shared across protocols; per-protocol guides
524
+ reference them rather than re-defining them.
525
+
526
+ - **`LoopPhase.context_filter?: LoopContextCategory[]`** — closed enum
403
527
  with `'*'` wildcard. Drives per-phase memory selection at brief
404
- assembly time.
405
- - `LoopPhase.advance_gate?: StopCondition` — re-uses the StopCondition
406
- vocabulary as a phase-exit guard. When unmet, the driver emits a
407
- `phase_advance_blocked` system event (a non-artifact event in the
408
- journal) with a structured `gate_reason` and throws an actionable
409
- error. The default ideation `critique` advance_gate is
410
- `min_artifacts_by_type { type: 'critique', n: 3, scope: 'phase' }`.
411
- - `LoopProtocolConfig.iteration?: { cycle, max_iterations, exit_when }`
412
- wraps the inner critique↔revision loop. The FSM
413
- (`decideNextPhase` in `iteration-engine.ts`) handles cycle progress,
414
- exit_when predicates (`no_new_critique_artifacts` / `critic_signal`),
415
- and emits `max_iterations_reached` when the cap fires.
528
+ assembly time (ideation, implementation, research, debug all use it).
529
+ - **`LoopPhase.advance_gate?: StopCondition`** — re-uses the
530
+ `StopCondition` vocabulary as a phase-exit guard. When unmet, the driver
531
+ emits a `phase_advance_blocked` system event with a structured
532
+ `gate_reason` and throws an actionable error. Every protocol except
533
+ `review` ships at least one default gate.
534
+ - **`LoopProtocolConfig.iteration?: { cycle, max_iterations, exit_when }`**
535
+ wraps an inner cycle. The FSM (`decideNextPhase` in
536
+ `iteration-engine.ts`) handles cycle progress and the `exit_when`
537
+ predicates (`no_new_critique_artifacts`, `critic_signal`,
538
+ `command_green`), and emits `max_iterations_reached` when the cap
539
+ fires.
416
540
 
417
541
  Both new event kinds — `phase_advance_blocked` and
418
542
  `max_iterations_reached` — live in the same event journal as
@@ -420,6 +544,38 @@ Both new event kinds — `phase_advance_blocked` and
420
544
  artifacts (which would force every consumer to filter `is_system`
421
545
  before processing content).
422
546
 
547
+ ## Recovery and observability
548
+
549
+ Recovery of a dispatched turn is decision-driven, not marker-driven — a
550
+ recoverer reads the reservation record and acts on its
551
+ `(decision, launch.status, lease_deadline)` triple. The full set of
552
+ transitions and their handling lives in
553
+ [attempt-authority.md#recovery](./attempt-authority.md#recovery). Loop-level
554
+ recovery on top of that:
555
+
556
+ - **Projection repair before crossing.** The common dispatch choke point
557
+ replays create-or-validate operations for Assignment, AgentRun, claim and
558
+ slot while the grant is still armed. It crosses only after all four exist;
559
+ an already-crossed replay never acquires spawn authority again.
560
+ - **Terminal loop early-return.** Every mutating convergence
561
+ (`reconcileTurn`, `reconcileFailedTurn`) idempotent no-ops on a closed
562
+ loop and still releases the coordinator claim.
563
+ - **Journal crash recovery.** `max(event.seq) > thread.version` triggers
564
+ a synchronous journal replay before any new mutation proceeds (see the
565
+ commit protocol below).
566
+ - **Superseded-turn guard.** A newer turn taking over a slot binds
567
+ `slot.current_turn_id`; a late reconcile of the old turn no-ops.
568
+ - **Contradictions.** A turn-keyed completed+failed pair on the same
569
+ attempt withholds convergence and journals a `run_blocked` runtime
570
+ event with `status_reason: turn_evidence_contradiction`.
571
+
572
+ Observability is split across four surfaces — see
573
+ [attempt-authority.md#surfaces-and-their-roles](./attempt-authority.md#surfaces-and-their-roles).
574
+ In short: the `TurnReservation` record + launch decision cell is
575
+ **authoritative**; `LoopEvent` is **causal**; `RuntimeEvent` is
576
+ **telemetry**; the legacy `events.jsonl` stream is **compatibility-only**.
577
+ No consumer looks past its role. There is no fifth journal.
578
+
423
579
  ## Persistence
424
580
 
425
581
  ```
@@ -427,6 +583,7 @@ before processing content).
427
583
  threads/<id>.json # main state
428
584
  events/<id>.jsonl # append-only journal (seq/version authoritative)
429
585
  locks/<id>.lock # per-loop exclusive lock (all intents on an existing loop, and opt-out `open`)
586
+ locks/<id>.lock.takeovers/<sha256(mutation_id)>.lock # immutable election claim for reaping one dead lock generation
430
587
  locks/open/<agent_id>/<client_request_id>.lock # idempotent-`open` lock keyed on idempotency scope
431
588
  idempotency/<id>/<client_request_id>.json # 24h cache of completed mutation responses (one loop)
432
589
  idempotency-open/<agent_id>/<client_request_id>.json # 24h cache for `open` intent (no loop_id yet)
@@ -452,24 +609,40 @@ before processing content).
452
609
  }
453
610
  ```
454
611
 
455
- **Server-owned lease renewal, bounded by a hard deadline.** `lease_until` is set to `acquired_at + 60 s` on lock acquisition. `hard_deadline` is set once at acquisition time to `acquired_at + max_mutation_duration` and **never moves**. The MCP handler spawns an internal heartbeat that rewrites `lease_until = now + 60 s` every 30 s while the mutation is still in flight — but **only as long as `now < hard_deadline`**. Heartbeat updates use the same temp-file + atomic-rename pattern as `thread.json`: write the full lock blob to a sibling temp file, fsync it, atomic-rename over `locks/<id>.lock`, fsync the directory. Readers therefore either see the old blob or the new blob, never a torn partial JSON document. The heartbeat refuses to renew past the deadline, the handler is instructed to abort its mutation, and the lock becomes reclaimable after the next `grace` window.
612
+ **Lease/deadline fields are diagnostic today.** `lease_until` is initialized to
613
+ `acquired_at + 60 s`; `hard_deadline` is initialized from the intent's expected
614
+ maximum duration and never moves. The current synchronous implementation does
615
+ **not** run a lease-renewal heartbeat and does not reap a lock merely because
616
+ either timestamp elapsed. This is deliberate: on Windows, a live process may be
617
+ suspended longer than a deadline and later resume. Until every committing write
618
+ has its own fence check, elapsed time alone is not proof that takeover is safe.
456
619
 
457
620
  Default `max_mutation_duration` per intent:
458
621
 
459
622
  | Intent | `max_mutation_duration` | Rationale |
460
623
  |---|---|---|
461
- | `open`, `turn`, `advance`, `pause`, `resume`, `close` | 30 s | Pure state transitions. `turn` is fire-and-forget the dispatch call is kicked off inside the lock but the handler does not await its completion, so the lock window stays tight. |
624
+ | `open`, `turn`, `advance`, `pause`, `resume`, `close` | 30 s | Short state transitions. A trusted `turn(dispatch:true)` prepares/launches through AttemptAuthority outside the loop commit window, so worker duration never extends this lock. |
462
625
  | `add_artifact`, `complete_turn` | 60 s | May write small external ref files. |
463
626
 
464
- The cap is configurable in `config.yaml` under `loops.max_mutation_duration_ms` (per-intent map). A wedged handler therefore cannot hold the lock past its intent-specific deadline; after the deadline, the lock is reclaimable by any recovery pass per the rules below. Callers never interact with the lease or deadline — both are server-internal.
627
+ These values describe the expected mutation window and support diagnostics.
628
+ They are not automatic takeover thresholds. Callers never interact with them.
465
629
 
466
- **Why `turn` is fire-and-forget.** If `turn` awaited the downstream CLI/MCP call synchronously, a single slow agent (e.g. a 5-minute Codex review) would hold the per-loop lock and block every other mutation — a head-of-line-blocking hazard. Instead, the handler issues the dispatch, captures the `assignment_id`, writes `slot.status='assigned'`, commits, and releases the lock. The spawned process reports back later via `complete_turn`, which takes its own (short) lock. This is also consistent with brainclaw's existing dispatch contract: assignments are always async.
630
+ **Why turn dispatch never holds the loop lock while a worker runs.** A single
631
+ slow agent must never block every other loop mutation. Coordination, sequence
632
+ dispatch, and trusted `turn(dispatch:true)` use AttemptAuthority crossing, but
633
+ process preparation/launch happens
634
+ outside the short loop-state commit. The worker reports back later via
635
+ harvest/reconciliation or `complete_turn`, which takes its own short lock.
467
636
 
468
637
  **Commit protocol (lock-file CAS with intra-lock idempotency):**
469
638
 
470
639
  Before step 1, for the opt-out `open` path only (no `client_request_id`), the handler **pre-mints** the `loop_id` (ULID). Every other intent already has a `loop_id`; the idempotent `open` path postpones minting to step 3 so the idempotency cache can guard it.
471
640
 
472
- 1. **Acquire lock.** Open the appropriate lock path (see *Lock scoping* above) with `O_CREAT | O_EXCL` (POSIX) or `CreateFile` with exclusive share mode (Windows) and write the owner blob. On `EEXIST`, retry with jittered backoff (10 ms base, capped at 500 ms total). After timeout, fail with `lock_timeout`. Start the lease-renewal heartbeat (bounded by `hard_deadline`).
641
+ 1. **Acquire lock.** Write the complete owner blob to a unique sibling temp file,
642
+ then hard-link that file to the lock path. Hard-link creation is the shared
643
+ create-if-absent primitive on POSIX and Windows; only one contender can win.
644
+ Remove the temp file after linking. On `EEXIST`, retry with jittered backoff
645
+ (10 ms base, capped at 500 ms total). After timeout, fail with `lock_timeout`.
473
646
  2. **Idempotency short-circuit (inside lock).** If the caller supplied `client_request_id`:
474
647
  - For mutations on an existing loop: look up `idempotency/<id>/<client_request_id>.json`.
475
648
  - For `open`: look up `idempotency-open/<agent_id>/<client_request_id>.json`.
@@ -480,23 +653,45 @@ Before step 1, for the opt-out `open` path only (no `client_request_id`), the ha
480
653
  - After replay/auth, if the caller supplied `expected_version` and `thread.version !== expected_version`: append a `LoopConflictRecord` to `conflicts/<id>.jsonl` (observability only, no `seq`, no `version` bump), release the lock, and return `{ status: 'error', code: 'version_conflict', actual_version }`.
481
654
  - For idempotent `open` (locked on the idempotency scope): mint a fresh random `loop_id` (ULID) here. This is the only id-mint point for the idempotent path.
482
655
  - For opt-out `open`: `loop_id` was already minted before step 1; nothing to do here.
483
- 4. **Append event** *(fenced)*. Fence check: re-read `locks/<id>.lock` and verify its `mutation_id` still equals the value this handler wrote at step 1. If it differs, the lock has been reaped and a different handler owns the loop — **abort immediately without writing**, return `{ status: 'error', code: 'lock_lost' }`. Otherwise, write the new event to `events/<loop_id>.jsonl` with `seq = prev_seq + 1` (or `seq = 1` for `open`) and the handler's own `mutation_id` (ULID, minted at step 1 into the lock blob). Fsync the file.
484
- 5. **Atomic-rename thread** *(fenced)*. Repeat the same fence check on `locks/<id>.lock`. On mismatch, abort. Otherwise, write the new thread state (with `version = prev_version + 1`, or `version = 1` for `open`, and the same `mutation_id`) to a temp file, atomic-rename over `threads/<loop_id>.json`, fsync the directory.
656
+ 4. **Entry fence, then commit.** Immediately before the synchronous verb, re-read
657
+ the lock and verify its `mutation_id`. On mismatch, abort with `lock_lost`.
658
+ The verb appends its event and materializes `thread.json`; journal-first
659
+ recovery catches a crash between those writes. There is currently no second
660
+ fence between event and thread writes, which is why automatic reaping is
661
+ restricted to owners proven dead on the local host.
662
+ 5. **Atomic-rename thread.** Write the next thread state with `version =
663
+ prev_version + 1` (or `1` for `open`) and the mutation id associated with the
664
+ verb, then atomic-rename it over `threads/<loop_id>.json`.
485
665
  6. **Persist idempotency record.** If `client_request_id` was supplied, write `{ response, request_hash, stored_at }` to the relevant idempotency path. (For `open`, the stored response includes the minted `loop_id` so retries get the same id back.)
486
- 7. **Release lock.** Stop the lease-renewal heartbeat and remove the lock file.
666
+ 7. **Release lock.** Re-read the lock and remove it only when its `mutation_id`
667
+ still belongs to this handler.
487
668
 
488
- **Fencing token — what the re-read catches.** Every handler writes its own `mutation_id` into the lock blob at step 1. If the handler later blocks on a slow fs call or a dispatch kickoff and the deadline/liveness rules kick in, the recovery pass removes the lock. A different handler can then acquire a fresh lock with a **different** `mutation_id`. The late-unblocking handler's fence re-read at steps 4 and 5 will see the foreign `mutation_id` and abort cleanly — no write, no corruption, no phantom events in the journal. This closes the "late unblock after reap" hole: lock ownership is checked not only at acquisition but at every committing I/O point.
669
+ **Fencing token — current guarantee.** Every handler writes its own `mutation_id`
670
+ into the lock blob and checks it at verb entry. Release also compares that token,
671
+ so an old owner cannot remove a different generation. Because a live local owner
672
+ is never reaped, it cannot resume after takeover inside the synchronous verb.
673
+ Enabling deadline-based, remote-host, or asynchronous takeover in a future slice
674
+ requires propagating the fence check to every journal, projection, thread and
675
+ idempotency commit first.
489
676
 
490
677
  The `event.seq` and `thread.version` advance in lockstep — a successful commit produces exactly one new event with `seq = new_version`. Conflict records in `conflicts/<id>.jsonl` are out-of-band and never affect `seq` or `version`. The shared `mutation_id` on both committed files pins which event materialized which thread revision. Because step 3 always replays `events/<id>.jsonl` before a new CAS decision, a stale materialized thread cannot cause the next writer to append a journal event "ahead" of `thread.json`; the journal remains authoritative, and each new mutation must first catch the thread up to it.
491
678
 
492
- **Stale-lock recovery (owner-liveness + deadline, not age-based):**
679
+ **Stale-lock recovery (proof-based and generation-fenced):**
493
680
 
494
- - Read the lock blob. If `now > hard_deadline` the mutation exceeded its intent-specific cap → remove the lock regardless of liveness.
495
- - Else if `host_id === current_host_id` and no process with `pid` exists (checked via `kill -0` / `OpenProcess`), the owner is dead remove the lock.
496
- - Else if `now > lease_until + grace` (default grace = 30 s) and the owner has not renewed, treat as abandoned → remove the lock.
497
- - Else the lock is considered live; callers keep retrying.
681
+ - If `host_id === current_host_id` and no process with `pid` exists (checked via
682
+ `kill -0` / `OpenProcess`), the owner is proven dead and its generation is
683
+ eligible for automatic recovery.
684
+ - A contender first creates
685
+ `<lock>.takeovers/<sha256(observed_mutation_id)>.lock` with the same hard-link
686
+ create-if-absent primitive. Only that elected reaper may re-read and unlink the
687
+ observed generation. This prevents the Windows ABA race where a late reaper
688
+ deletes a freshly acquired generation.
689
+ - A live local PID, a different host, or elapsed lease/deadline fields fail
690
+ closed. The caller times out; an operator can inspect the blob before explicit
691
+ recovery.
498
692
 
499
- The three rules are independent: `hard_deadline` bounds pathological "heartbeat alive but mutation wedged" cases; liveness check bounds crash cases; `lease_until + grace` bounds network/fs stalls. This fully replaces the unsafe "age > 10 s ⇒ reap" rule — a legitimate writer blocked on a slow fs call is no longer killed by age alone, but is still bounded by the intent-specific deadline.
693
+ This preserves short per-loop serialization without a global Loop Engine lock.
694
+ Independent loops and immutable AttemptAuthority cells remain parallel.
500
695
 
501
696
  **Journal crash recovery:**
502
697
 
@@ -555,6 +750,9 @@ table above rather than treating review as the default abstraction.
555
750
 
556
751
  ## Related
557
752
 
753
+ - [attempt-authority.md](./attempt-authority.md) — identity, dispatch decisions and spawn authority for every turn
754
+ - [P0B projection-boundary tests](../../tests/unit/loops-p0b-projections-before-crossing.test.ts) — crash/replay coverage around the common pre-crossing boundary
755
+ - [Per-protocol guides](../loops/) — review / ideation / implementation / research / debug
558
756
  - [plans-and-claims.md](plans-and-claims.md)
559
757
  - [coordination.md](coordination.md)
560
758
  - [dispatch-lifecycle.md](dispatch-lifecycle.md) — entity FSMs (loop / assignment / agent_run / claim), brief-ack semantics, log-file diagnostic playbook
@@ -562,13 +760,4 @@ table above rather than treating review as the default abstraction.
562
760
  - pln#394 `feat/loop-engine-mvp`
563
761
  - pln#395 `feat/review-loop-protocol`
564
762
  - pln#392 `doc/mcp-versioning-and-surface-governance` (prerequisite)
565
-
566
- ## Review-specific reliability notes
567
-
568
- Review loops retain an extra exactly-once fix-cycle implementation because they
569
- can automatically redispatch after `request_changes`. A reviewer writes
570
- `review_verdict` and `review_summary` to `LANE-RESULT.json`; harvest maps this
571
- to the loop, auto-closes on approval, and boundedly redispatches fix work when
572
- appropriate. This is review-specific automation, not a limit on the other
573
- workflow kinds. Set `BRAINCLAW_TURN_OWNED_REVIEW=0` (also `false`/`off`/`no`)
574
- only to fall back to the legacy review finalizer.
763
+ - pln#676 / dec#171 — attempt-authority rollout