brainclaw 1.26.1 → 1.27.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 (91) hide show
  1. package/README.md +41 -12
  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 +87 -14
  8. package/dist/commands/mcp-catalog.js +42 -18
  9. package/dist/commands/mcp-schemas.generated.js +44 -0
  10. package/dist/commands/mcp-write-claims.js +128 -1
  11. package/dist/commands/mcp-write-coordination.js +146 -76
  12. package/dist/core/agent-capability.js +1 -1
  13. package/dist/core/agent-files.js +21 -21
  14. package/dist/core/agentrun-reconciler.js +148 -22
  15. package/dist/core/agentruns.js +254 -29
  16. package/dist/core/assignment-request-schema.js +7 -0
  17. package/dist/core/assignment-sweeper.js +5 -3
  18. package/dist/core/assignments.js +131 -33
  19. package/dist/core/claim-request-schema.js +7 -0
  20. package/dist/core/claims.js +53 -2
  21. package/dist/core/dispatch-status.js +16 -6
  22. package/dist/core/dispatcher.js +51 -51
  23. package/dist/core/entity-operations.js +20 -0
  24. package/dist/core/events.js +4 -0
  25. package/dist/core/execution-adapters.js +160 -14
  26. package/dist/core/execution-contract.js +345 -0
  27. package/dist/core/execution.js +130 -16
  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 +207 -0
  40. package/dist/core/loops/artifact-contract.js +11 -0
  41. package/dist/core/loops/attempt-authority.js +476 -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/evidence.js +187 -0
  48. package/dist/core/loops/facade-schema.js +41 -10
  49. package/dist/core/loops/gate-policy.js +485 -0
  50. package/dist/core/loops/impl-bind.js +37 -79
  51. package/dist/core/loops/index.js +9 -0
  52. package/dist/core/loops/iteration-engine.js +31 -19
  53. package/dist/core/loops/kind-policies.js +90 -0
  54. package/dist/core/loops/lock.js +71 -13
  55. package/dist/core/loops/reconcile-turn.js +235 -18
  56. package/dist/core/loops/result-reducers.js +99 -10
  57. package/dist/core/loops/store.js +30 -3
  58. package/dist/core/loops/turn-execution.js +480 -0
  59. package/dist/core/loops/types.js +113 -2
  60. package/dist/core/loops/verbs.js +332 -99
  61. package/dist/core/loops/verify-command.js +31 -8
  62. package/dist/core/loops/workspace-digest.js +54 -0
  63. package/dist/core/protocol-tool-policy.js +44 -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 +35 -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/PROTOCOL.md +6 -4
  73. package/docs/cli.md +49 -1
  74. package/docs/concepts/attempt-authority.md +407 -0
  75. package/docs/concepts/evidence-attestations.md +135 -0
  76. package/docs/concepts/execution-contract.md +166 -0
  77. package/docs/concepts/harness-adapters.md +166 -0
  78. package/docs/concepts/ideation-loop.md +5 -4
  79. package/docs/concepts/loop-engine.md +348 -133
  80. package/docs/index.md +4 -1
  81. package/docs/integrations/codex.md +3 -3
  82. package/docs/integrations/hermes.md +42 -3
  83. package/docs/integrations/mcp.md +75 -9
  84. package/docs/loops/debug.md +144 -0
  85. package/docs/loops/ideation.md +158 -0
  86. package/docs/loops/implementation.md +154 -0
  87. package/docs/loops/research.md +136 -0
  88. package/docs/loops/review.md +200 -0
  89. package/docs/mcp-schema-changelog.md +14 -5
  90. package/docs/product/agent-first-model.md +33 -33
  91. package/package.json +1 -1
@@ -1,18 +1,46 @@
1
1
  # Loop engine
2
2
 
3
- brainclaw coordinates many agents against shared state.
4
- The Loop engine turns repetitive multi-turn workflows
5
- — reviews, ideation rounds, implementation handoffs —
6
- into **first-class, persistable, automatable objects**.
7
-
8
- Status: design draft v8 (pln#394 step 1). v6 added a hard mutation deadline + consistent opt-out-`open` and unified terminology. v7 made `turn` strictly async and fenced committing writes with `mutation_id` re-reads. v8 applies Codex's follow-up lock-check (cnd#580 / `dec_4ba1a20f`) and introduces the symmetric review-and-fix protocol mode. Codex-authored fixes: the commit protocol now makes **journal replay before CAS normative**, `complete_turn` and any future slot-bound mutation now require the caller's `agentId` to match the slot owner (with `created_by` as the only admin fallback), and lock-heartbeat renewals are written by temp-file + atomic rename so fence reads always observe a coherent JSON blob. Final symmetric-mode integration: `open` accepts `mode: 'asymmetric' | 'symmetric'`, the resolved selection is persisted on the loop thread for deterministic resume/turn handling, and per-turn execution degrades cleanly to asymmetric behavior when a slot cannot safely apply fixes.
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
+
8
+ Status: **shipped**. `bclaw_loop` exposes the persistent engine and its five
9
+ built-in protocols; `bclaw_coordinate` and `bclaw_dispatch` add ergonomic
10
+ shortcuts for ideation and review. This document retains the RFC-level
11
+ concurrency contract and implementation history where it explains an
12
+ invariant, but its operational sections describe the surface available today.
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).
9
37
 
10
38
  ## Why
11
39
 
12
- Today, recurring multi-agent work is done by hand:
13
- an operator copies a diff, pastes it to a reviewer, collects the findings,
14
- pastes them back to the author, re-asks for a re-review.
15
- Each round is glue work, lost context, and copy-paste errors.
40
+ Without a loop, recurring work is easy to reduce to manual ping-pong:
41
+ an operator relays a proposal and its critique, asks an implementer to retry a
42
+ failed check, gathers research findings, or forwards review feedback. Each
43
+ round is glue work, lost context, and copy-paste errors.
16
44
 
17
45
  A Loop captures the whole cycle as state:
18
46
  *participants, phases, current position, artifacts, stop criteria*.
@@ -34,6 +62,72 @@ A Loop is a **persistent thread of structured work** with:
34
62
  A Loop stores *references* to existing objects — it never duplicates them.
35
63
  Claims, handoffs, and candidates remain the source of truth for their own data.
36
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
+
37
131
  ## Data model
38
132
 
39
133
  ```ts
@@ -60,6 +154,10 @@ interface LoopThread {
60
154
  artifacts: LoopArtifact[];
61
155
  linked?: LoopLinks; // top-level context only (plan/sequence). Other refs live on artifacts/slots.
62
156
  stop_condition?: StopCondition;
157
+ evidence_policy?: { // absent only on explicit/pre-policy threads
158
+ version: 'gate-policy-v1';
159
+ mode: 'shadow' | 'strict';
160
+ };
63
161
 
64
162
  created_at: string; // ISO
65
163
  updated_at: string;
@@ -71,12 +169,19 @@ type LoopStatus = 'open' | 'paused' | 'completed' | 'blocked' | 'cancelled';
71
169
  type ReviewMode = 'asymmetric' | 'symmetric';
72
170
 
73
171
  interface LoopProtocolConfig {
74
- 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
75
178
  }
76
179
 
77
180
  interface LoopPhase {
78
181
  name: string;
79
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;
80
185
  }
81
186
 
82
187
  interface LoopSlot {
@@ -87,7 +192,8 @@ interface LoopSlot {
87
192
  assignment_id?: string; // set when a turn is dispatched
88
193
  claim_id?: string; // for execution loops, the claim held by this slot
89
194
  phase?: string; // which phase this slot currently participates in (supports parallel slots per phase)
90
- 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
91
197
  }
92
198
 
93
199
  interface LoopArtifact {
@@ -96,8 +202,9 @@ interface LoopArtifact {
96
202
  type: string; // "finding" | "synthesis" | "verdict" | "plan_draft" | ...
97
203
  ref?: LoopRef; // preferred: link to an existing primitive
98
204
  body?: string; // inline content ≤ 4 KB; else force `ref`
99
- produced_by?: SlotId;
205
+ produced_by?: string; // derived server-side from slot/engine/coordinator context
100
206
  produced_at: string;
207
+ evidence?: EvidenceEnvelope; // server-sealed; never caller-authored
101
208
  }
102
209
 
103
210
  type LoopRef =
@@ -119,7 +226,10 @@ type AtomicStopCondition =
119
226
  | { kind: 'phase_reached'; phase: string }
120
227
  | { kind: 'reviewer_green' } // an `accepted` verdict artifact in any phase
121
228
  | { kind: 'max_iterations'; n: number } // hard cap; on hit, close with status=blocked
229
+ | { kind: 'min_iterations'; n: number }
122
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' }
123
233
  | { kind: 'manual' }; // only closes on explicit close
124
234
 
125
235
  type StopCondition =
@@ -128,6 +238,9 @@ type StopCondition =
128
238
  | { kind: 'all'; conditions: StopCondition[] }; // AND — every clause must match
129
239
 
130
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.
131
244
  interface LoopEventBase {
132
245
  event_id: string; // ULID
133
246
  loop_id: LoopId;
@@ -142,6 +255,7 @@ type LoopEvent =
142
255
  | (LoopEventBase & { kind: 'phase_advanced'; from_phase: string; to_phase: string; iteration: number; reason?: string })
143
256
  | (LoopEventBase & { kind: 'turn_assigned'; slot_id: SlotId; phase: string; assignment_id?: string; input?: string; retry_of?: string /* prior event_id */ })
144
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 })
145
259
  | (LoopEventBase & { kind: 'artifact_added'; artifact_id: string; phase: string; type: string; produced_by?: SlotId })
146
260
  | (LoopEventBase & { kind: 'linked'; target: LoopRef })
147
261
  | (LoopEventBase & { kind: 'paused'; reason?: string })
@@ -219,18 +333,43 @@ file lists which artifact types are ref-based and which use inline JSON bodies.
219
333
 
220
334
  ## Lifecycle verbs
221
335
 
222
- 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.
223
341
 
224
342
  - **open** — create a new loop. Inserts `opened` event; `current_phase` set to `phases[0].name`.
225
- - **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`.
226
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.
227
352
  - **close** — terminal: set `status` to `completed | cancelled | blocked` and `closed_at`. Inserts `closed`.
228
353
 
229
- Two auxiliary verbs cover quality of life:
354
+ Additional shared and engine-owned actions complete the lifecycle:
230
355
 
231
356
  - **pause** / **resume** — suspend a loop without closing (e.g. waiting on an external input).
232
357
  - **add_artifact** — attach an artifact to a phase without moving on.
233
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.
234
373
 
235
374
  ## MCP facade: `bclaw_loop(intent)`
236
375
 
@@ -246,16 +385,21 @@ interface BclawLoopCallerEnvelope {
246
385
 
247
386
  // Per-intent payloads. Every mutating intent supports `expected_version` + `client_request_id`.
248
387
  type BclawLoopInput = BclawLoopCallerEnvelope & (
249
- | { 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' */ }
250
- | { intent: 'turn'; loop_id: LoopId; slot_id?: SlotId; role?: string; input?: string; dispatch?: boolean; expected_version?: number }
251
- | { 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' }
252
392
  | { intent: 'advance'; loop_id: LoopId; to_phase?: string; reason?: string; force?: boolean; expected_version?: number }
253
- | { 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 }
254
394
  | { intent: 'pause'; loop_id: LoopId; reason?: string; expected_version?: number }
255
395
  | { intent: 'resume'; loop_id: LoopId; expected_version?: number }
256
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 }
257
401
  | { intent: 'get'; loop_id: LoopId; include_events?: boolean }
258
- | { 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 }
259
403
  );
260
404
 
261
405
  // Standard facade envelope, matching bclaw_work / bclaw_coordinate output shape.
@@ -281,6 +425,18 @@ type NextExpectedHint =
281
425
  | { action: 'close'; intent: 'bclaw_loop.close'; reason: string };
282
426
  ```
283
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
+
284
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).
285
441
 
286
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.
@@ -291,16 +447,36 @@ type NextExpectedHint =
291
447
 
292
448
  > **Caller note.** For `request_hash` to match on retry, the caller must replay the request body byte-for-byte, including any volatile fields it chose to include (timestamps, generated ids in the payload). Retries that differ in such fields will be treated as distinct requests and rejected with the reuse error. Practical rule: build the request once, snapshot it, and resend that exact snapshot on retry. The caller envelope itself (agent, agentId, client_request_id) is excluded from the hash.
293
449
 
294
- ## Default protocols
295
-
296
- Each `kind` ships a default `phases[]` and `stop_condition`. Users can override either at `open` time.
297
-
298
- | kind | phases | default stop_condition |
299
- |---|---|---|
300
- | `review` | `change_summary` → `findings` → `author_response` → `followup_review` → `verdict` | `reviewer_green` OR `max_iterations: 3` |
301
- | `ideation` | `proposal` → `critique` ↔ `revision` → `synthesis` (with iteration block + per-phase `context_filter` + `advance_gate` ≥3 critique artifacts; see [ideation-loop.md](./ideation-loop.md)) | `artifact_produced { phase: synthesis, type: plan_draft }` |
302
- | `implementation` | `sequence_build` → `dispatch` → `execute` → `self_check` → `handoff_ready` | `artifact_produced { phase: handoff_ready, type: handoff }` |
303
- | `research` / `debug` | user-defined | `manual` |
450
+ ## Supported workflows
451
+
452
+ The engine is one control plane, not a review feature with a few extensions.
453
+ All five `LoopKind` values below ship a default protocol in
454
+ `src/core/loops/types.ts`; callers may override phases and stop conditions when
455
+ opening a loop. Review has the most automated coordinator shortcut, but it is
456
+ one workflow among the five.
457
+
458
+ | kind | default progression | normal entry point | converges on |
459
+ |---|---|---|---|
460
+ | `review` | `change_summary` → `findings` → `author_response` → `followup_review` → `verdict` | `bclaw_coordinate(intent="review", open_loop=true)` or `bclaw_dispatch(intent="review", openLoop=true)` | an accepted verdict or the bounded iteration cap |
461
+ | `ideation` | `proposal` → `critique` ↔ `revision` → `synthesis` | `bclaw_coordinate(intent="ideate")`; `preset="bootstrap"` selects the onboarding variant | a `plan_draft` synthesis; see [ideation-loop.md](./ideation-loop.md) |
462
+ | `implementation` | `bind` → `execute` ↔ `verify` → `handoff_ready` | `bclaw_loop(intent="open", kind="implementation", allow_orphan=true)`, then `bind` | a handoff after a green verification command, or a bounded blocked result |
463
+ | `research` | `investigate` ↔ `synthesize` → `conclude` | `bclaw_loop(intent="open", kind="research", allow_orphan=true)` | a synthesis after at least one finding per investigation round |
464
+ | `debug` | `reproduce` → `hypothesize` ↔ `isolate` ↔ `fix` → `handoff` | `bclaw_loop(intent="open", kind="debug", allow_orphan=true)` | a handoff after the reproducing command is green, or a bounded blocked result |
465
+
466
+ `allow_orphan=true` is an explicit acknowledgement for direct opens: the caller
467
+ will drive or dispatch the resulting loop rather than creating an inert thread.
468
+ The shared lifecycle verbs are `turn`, `complete_turn`, `advance`,
469
+ `add_artifact`, `pause`, `resume`, and `close`. Implementation loops additionally
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.
472
+
473
+ ### Clarification is a cross-cutting primitive
474
+
475
+ Clarification is deliberately not a sixth protocol. Any workflow can call
476
+ `request_input` to record an evidence-backed operator question, pause either a
477
+ slot or the whole loop, and resume through `provide_input`. This keeps a missing
478
+ decision from being confused with a review finding or a failed implementation
479
+ check.
304
480
 
305
481
  ## Relation to existing primitives
306
482
 
@@ -317,76 +493,50 @@ The Loop engine is a **control plane**; existing primitives remain the **data pl
317
493
 
318
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.
319
495
 
320
- ## Automation: extending `bclaw_coordinate(intent='review')`
321
-
322
- This is the user-visible promise of the MVP — manual review round-trips disappear.
323
-
324
- 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.
325
-
326
- When `bclaw_coordinate(intent='review', open_loop: true)` is called, it:
327
-
328
- 1. Creates the review candidate as today.
329
- 2. Opens a `review` loop via `bclaw_loop(intent: 'open', kind: 'review', ...)` with slots `{role: 'author', agent: caller}`, `{role: 'reviewer', agent: target}`.
330
- 3. Links the provided handoff/candidate to the loop as an artifact at `change_summary`.
331
- 4. Advances to `findings` and calls `bclaw_loop(intent: 'turn')` to dispatch to the reviewer.
332
- 5. On turn completion with a verdict artifact, auto-advances; `reviewer_green` stop closes.
333
- 6. On a `request_changes` verdict, the fix cycle re-dispatches the reviewer into the same worktree until `approve` or the `max_iterations` cap.
334
-
335
- **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`.
336
-
337
- **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.
338
-
339
- ### Symmetric review-AND-fix mode
340
-
341
- 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.
342
-
343
- 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.
344
-
345
- 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.
346
-
347
- 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.
348
-
349
- 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.
350
-
351
- ## Automation: extending `bclaw_coordinate(intent='ideate')`
352
-
353
- Shipped in v1.5.0 (pln#492). The full design — phases, context_filter,
354
- iteration block, advance_gate, brief assembly, system events,
355
- single-agent vs multi-agent UX — lives in [ideation-loop.md](./ideation-loop.md).
356
- Summary for the loop-engine perspective:
357
-
358
- - `bclaw_coordinate(intent='ideate', task=…, [targetAgents=[…]])` opens
359
- an ideation_loop with the caller as `champion` slot and the targets
360
- (when provided) as `critic` slots. The task is stored verbatim as
361
- the `proposal` artifact (sliced to the 4 KB body cap).
362
- - Single-agent mode (no `targetAgents`): the loop opens at the
363
- proposal phase and stops there. The champion drives the cycle
364
- manually via `bclaw_loop(intent='turn'|'advance')`. Useful when the
365
- loop's structure (memory filter, gate, iteration accounting) is
366
- what's wanted, not the multi-slot orchestration.
367
- - Multi-agent mode (explicit `targetAgents`): the driver advances
368
- proposal → critique and dispatches a turn per critic with a brief
369
- assembled by `buildIdeationBrief` — context-filtered (critic sees
370
- only `traps + feedback + runtime_notes + critique_history`),
371
- BM25-ranked via `search()`, capped at 48 KB.
372
-
373
- The ideation_loop introduces three loop-engine extensions consumed by
374
- this driver:
375
-
376
- - `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
377
527
  with `'*'` wildcard. Drives per-phase memory selection at brief
378
- assembly time.
379
- - `LoopPhase.advance_gate?: StopCondition` — re-uses the StopCondition
380
- vocabulary as a phase-exit guard. When unmet, the driver emits a
381
- `phase_advance_blocked` system event (a non-artifact event in the
382
- journal) with a structured `gate_reason` and throws an actionable
383
- error. The default ideation `critique` advance_gate is
384
- `min_artifacts_by_type { type: 'critique', n: 3, scope: 'phase' }`.
385
- - `LoopProtocolConfig.iteration?: { cycle, max_iterations, exit_when }`
386
- — wraps the inner critique↔revision loop. The FSM
387
- (`decideNextPhase` in `iteration-engine.ts`) handles cycle progress,
388
- exit_when predicates (`no_new_critique_artifacts` / `critic_signal`),
389
- 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.
390
540
 
391
541
  Both new event kinds — `phase_advance_blocked` and
392
542
  `max_iterations_reached` — live in the same event journal as
@@ -394,6 +544,38 @@ Both new event kinds — `phase_advance_blocked` and
394
544
  artifacts (which would force every consumer to filter `is_system`
395
545
  before processing content).
396
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
+
397
579
  ## Persistence
398
580
 
399
581
  ```
@@ -401,6 +583,7 @@ before processing content).
401
583
  threads/<id>.json # main state
402
584
  events/<id>.jsonl # append-only journal (seq/version authoritative)
403
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
404
587
  locks/open/<agent_id>/<client_request_id>.lock # idempotent-`open` lock keyed on idempotency scope
405
588
  idempotency/<id>/<client_request_id>.json # 24h cache of completed mutation responses (one loop)
406
589
  idempotency-open/<agent_id>/<client_request_id>.json # 24h cache for `open` intent (no loop_id yet)
@@ -426,24 +609,40 @@ before processing content).
426
609
  }
427
610
  ```
428
611
 
429
- **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.
430
619
 
431
620
  Default `max_mutation_duration` per intent:
432
621
 
433
622
  | Intent | `max_mutation_duration` | Rationale |
434
623
  |---|---|---|
435
- | `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. |
436
625
  | `add_artifact`, `complete_turn` | 60 s | May write small external ref files. |
437
626
 
438
- 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.
439
629
 
440
- **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.
441
636
 
442
637
  **Commit protocol (lock-file CAS with intra-lock idempotency):**
443
638
 
444
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.
445
640
 
446
- 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`.
447
646
  2. **Idempotency short-circuit (inside lock).** If the caller supplied `client_request_id`:
448
647
  - For mutations on an existing loop: look up `idempotency/<id>/<client_request_id>.json`.
449
648
  - For `open`: look up `idempotency-open/<agent_id>/<client_request_id>.json`.
@@ -454,23 +653,45 @@ Before step 1, for the opt-out `open` path only (no `client_request_id`), the ha
454
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 }`.
455
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.
456
655
  - For opt-out `open`: `loop_id` was already minted before step 1; nothing to do here.
457
- 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.
458
- 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`.
459
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.)
460
- 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.
461
668
 
462
- **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.
463
676
 
464
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.
465
678
 
466
- **Stale-lock recovery (owner-liveness + deadline, not age-based):**
679
+ **Stale-lock recovery (proof-based and generation-fenced):**
467
680
 
468
- - Read the lock blob. If `now > hard_deadline` → the mutation exceeded its intent-specific cap → remove the lock regardless of liveness.
469
- - 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.
470
- - Else if `now > lease_until + grace` (default grace = 30 s) and the owner has not renewed, treat as abandoned → remove the lock.
471
- - 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.
472
692
 
473
- 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.
474
695
 
475
696
  **Journal crash recovery:**
476
697
 
@@ -482,8 +703,8 @@ The three rules are independent: `hard_deadline` bounds pathological "heartbeat
482
703
 
483
704
  ## Routing and multi-instance
484
705
 
485
- - Discussion loops (`review`, `ideation`) route by `slot_id` — the engine writes to the slot's agent inbox via the existing coordinate path.
486
- - Execution loops (`implementation`) route by `claim_id` — preserved from the claim-routed model already in use.
706
+ - Collaborative loops (`review`, `ideation`, and `research`) route turns by `slot_id` — the engine writes to the slot's agent inbox via the existing coordinate path.
707
+ - Execution-oriented loops (`implementation` and `debug`) can route work by `claim_id`, preserving the claim-routed model already in use.
487
708
  - `session_id` is not a routing key; it remains observability-only. This is consistent with `architecture_session_centric_identity` in memory.
488
709
 
489
710
  ### Project resolution gate (pln#521 P1)
@@ -519,16 +740,19 @@ Status after Codex schema review (cnd#574 / `dec_be66ccbf`, verdict `needs_revis
519
740
  4. **Reopening a closed loop** — **Deferred.** `close` is terminal in MVP. Fixup reuse is done by opening a new loop that `linked` references the original.
520
741
  5. **Artifact size cap** — **Resolved: 4 KB inline `body`, else force `ref`.** Encoded in the `LoopArtifact` contract. Above 4 KB the handler rejects and suggests creating a `message` or `handoff` to reference.
521
742
 
522
- ## Next steps
743
+ ## Implementation status
523
744
 
524
- 1. If this final v8 review is green, lock the schema (this doc → `types.ts` in `src/core/loops/`).
525
- 2. Implement the four verbs (`open`, `turn`, `advance`, `close`) with the 2-phase-commit persistence described above.
526
- 3. Wire `bclaw_loop` into the MCP surface (pending pln#392 versioning policy).
527
- 4. Build the `review` protocol end-to-end (pln#395) as the first user-visible loop.
528
- 5. Add the `open_loop` opt-in on the existing `bclaw_coordinate(intent='review')` — the first manual-process killer.
745
+ The historical acceptance items in this RFC are complete: the verbs are exposed
746
+ through `bclaw_loop`, built-in protocols are defined in
747
+ `src/core/loops/types.ts`, and review/ideation receive coordinator shortcuts.
748
+ Future protocol work should extend the shared engine and update the workflow
749
+ table above rather than treating review as the default abstraction.
529
750
 
530
751
  ## Related
531
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
532
756
  - [plans-and-claims.md](plans-and-claims.md)
533
757
  - [coordination.md](coordination.md)
534
758
  - [dispatch-lifecycle.md](dispatch-lifecycle.md) — entity FSMs (loop / assignment / agent_run / claim), brief-ack semantics, log-file diagnostic playbook
@@ -536,13 +760,4 @@ Status after Codex schema review (cnd#574 / `dec_be66ccbf`, verdict `needs_revis
536
760
  - pln#394 `feat/loop-engine-mvp`
537
761
  - pln#395 `feat/review-loop-protocol`
538
762
  - pln#392 `doc/mcp-versioning-and-surface-governance` (prerequisite)
539
-
540
- ## Loops available today
541
-
542
- The loop surface exposed over MCP is intentionally narrow:
543
-
544
- - **Review loops** — `bclaw_coordinate(intent="review", open_loop=true, review_mode="asymmetric"|"symmetric", targetAgents=[…])` opens the loop and dispatches the first turn. The reviewer's verdict is then harvested from `LANE-RESULT.json` (`review_verdict`) and **auto-advances/closes the loop on approve** — no manual driving needed for the approve path (pln#628 Focus 4B). `bclaw_loop(intent="turn"|"complete_turn"|"advance"|"close")` remains available to drive turns by hand (e.g. the `request_changes` fix cycle, or a human-operated slot).
545
- - **Turn-owned exactly-once fix cycle (default, pln#630).** The autonomous `request_changes` fix-cycle re-dispatch runs through the turn-owned attempt state machine (immutable attempt record + atomic launch fence → spawned at most once; `reconcileTurn` finalizes from read-strict, turn-keyed evidence — the ack-wrapper's completion sentinel). It falls back to the legacy closer when a reviewer resolves to inbox/manual (no sentinel) so the loop still converges. **Kill-switch:** set `BRAINCLAW_TURN_OWNED_REVIEW=0` (also `false`/`off`/`no`) to revert review finalization to the legacy presence-based closer.
546
- - **Ideation loops** — `bclaw_coordinate(intent="ideate", preset="bootstrap")` opens an ideation loop from a preset.
547
-
548
- Custom phase lists (`LoopPhase[]`) and bespoke `StopCondition` logic exist in the loop engine internally, but are **not** exposed through the MCP facade today: `CoordinateRequestSchema` accepts only `open_loop`, `review_mode`, `preflight`, `ref`, and `preset` — no `phases` or `stop_condition` — and the standalone `bclaw_loop` tool does not expose an `open` intent. Programmatic construction of ad-hoc loops is therefore internal / future work until the facade is extended.
763
+ - pln#676 / dec#171 — attempt-authority rollout