brainclaw 0.29.2 → 1.5.4

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 (197) hide show
  1. package/LICENSE +21 -74
  2. package/README.md +199 -176
  3. package/dist/brainclaw-vscode.vsix +0 -0
  4. package/dist/cli.js +710 -25
  5. package/dist/commands/accept.js +3 -0
  6. package/dist/commands/add-step.js +11 -26
  7. package/dist/commands/agent-board.js +70 -3
  8. package/dist/commands/audit.js +19 -0
  9. package/dist/commands/check-policy.js +54 -0
  10. package/dist/commands/check-security-mcp.js +145 -0
  11. package/dist/commands/check-security.js +106 -0
  12. package/dist/commands/claim-resource.js +1 -0
  13. package/dist/commands/codev.js +672 -0
  14. package/dist/commands/compact.js +74 -0
  15. package/dist/commands/complete-step.js +16 -26
  16. package/dist/commands/constraint.js +8 -20
  17. package/dist/commands/decision.js +9 -20
  18. package/dist/commands/delete-plan.js +10 -12
  19. package/dist/commands/delete-step.js +16 -0
  20. package/dist/commands/dispatch.js +163 -0
  21. package/dist/commands/doctor.js +1122 -49
  22. package/dist/commands/enable-agent.js +1 -0
  23. package/dist/commands/export.js +280 -22
  24. package/dist/commands/handoff.js +33 -0
  25. package/dist/commands/harvest.js +189 -0
  26. package/dist/commands/hooks.js +82 -25
  27. package/dist/commands/inbox.js +169 -0
  28. package/dist/commands/init.js +38 -31
  29. package/dist/commands/install-hooks.js +71 -44
  30. package/dist/commands/link.js +89 -0
  31. package/dist/commands/list-claims.js +48 -3
  32. package/dist/commands/list-plans.js +129 -25
  33. package/dist/commands/loops-handlers.js +409 -0
  34. package/dist/commands/mcp-read-handlers.js +1628 -0
  35. package/dist/commands/mcp-schemas.generated.js +269 -0
  36. package/dist/commands/mcp.js +4224 -1501
  37. package/dist/commands/plan-resource.js +64 -0
  38. package/dist/commands/plan.js +12 -26
  39. package/dist/commands/prune.js +37 -2
  40. package/dist/commands/reflect.js +20 -7
  41. package/dist/commands/release-claim.js +11 -6
  42. package/dist/commands/release-notes.js +170 -0
  43. package/dist/commands/repair.js +210 -0
  44. package/dist/commands/run-profile.js +57 -0
  45. package/dist/commands/sequence.js +113 -0
  46. package/dist/commands/session-end.js +423 -14
  47. package/dist/commands/session-start.js +214 -41
  48. package/dist/commands/setup-security.js +103 -0
  49. package/dist/commands/setup.js +42 -4
  50. package/dist/commands/stale.js +109 -0
  51. package/dist/commands/switch.js +100 -2
  52. package/dist/commands/trap.js +14 -31
  53. package/dist/commands/update-handoff.js +63 -4
  54. package/dist/commands/update-plan.js +21 -28
  55. package/dist/commands/update-step.js +37 -0
  56. package/dist/commands/upgrade.js +313 -6
  57. package/dist/commands/usage.js +102 -0
  58. package/dist/commands/version.js +20 -0
  59. package/dist/commands/who.js +33 -5
  60. package/dist/commands/worktree.js +105 -0
  61. package/dist/core/actions.js +315 -0
  62. package/dist/core/agent-capability.js +610 -17
  63. package/dist/core/agent-context.js +7 -1
  64. package/dist/core/agent-files.js +1169 -85
  65. package/dist/core/agent-integrations.js +160 -5
  66. package/dist/core/agent-inventory.js +2 -0
  67. package/dist/core/agent-profiles.js +93 -0
  68. package/dist/core/agent-registry.js +162 -30
  69. package/dist/core/agentrun-reconciler.js +345 -0
  70. package/dist/core/agentruns.js +424 -0
  71. package/dist/core/ai-agent-detection.js +31 -10
  72. package/dist/core/archival.js +77 -0
  73. package/dist/core/assignment-sweeper.js +82 -0
  74. package/dist/core/assignments.js +367 -0
  75. package/dist/core/audit.js +30 -0
  76. package/dist/core/brainclaw-version.js +94 -2
  77. package/dist/core/candidates.js +93 -2
  78. package/dist/core/claims.js +419 -0
  79. package/dist/core/codev-metrics.js +77 -0
  80. package/dist/core/codev-personas.js +31 -0
  81. package/dist/core/codev-plan-gen.js +35 -0
  82. package/dist/core/codev-prompts.js +74 -0
  83. package/dist/core/codev-responses.js +62 -0
  84. package/dist/core/codev-rounds.js +218 -0
  85. package/dist/core/config.js +4 -0
  86. package/dist/core/context.js +381 -34
  87. package/dist/core/coordination.js +201 -6
  88. package/dist/core/cross-project.js +230 -16
  89. package/dist/core/default-profiles/doctor.yaml +11 -0
  90. package/dist/core/default-profiles/janitor.yaml +11 -0
  91. package/dist/core/default-profiles/onboarder.yaml +11 -0
  92. package/dist/core/default-profiles/reviewer.yaml +13 -0
  93. package/dist/core/dispatcher.js +1189 -0
  94. package/dist/core/duplicates.js +2 -2
  95. package/dist/core/entity-operations.js +450 -0
  96. package/dist/core/entity-registry.js +344 -0
  97. package/dist/core/events.js +106 -2
  98. package/dist/core/execution-adapters.js +154 -0
  99. package/dist/core/execution-context.js +63 -0
  100. package/dist/core/execution-profile.js +270 -0
  101. package/dist/core/execution.js +255 -0
  102. package/dist/core/facade-schema.js +81 -0
  103. package/dist/core/federation-cloud.js +99 -0
  104. package/dist/core/federation-message.js +52 -0
  105. package/dist/core/federation-transport.js +65 -0
  106. package/dist/core/gc-semantic.js +482 -0
  107. package/dist/core/governance.js +247 -0
  108. package/dist/core/guards.js +19 -0
  109. package/dist/core/ideation.js +72 -0
  110. package/dist/core/identity.js +110 -25
  111. package/dist/core/ids.js +6 -0
  112. package/dist/core/input-validation.js +2 -2
  113. package/dist/core/instruction-templates.js +344 -136
  114. package/dist/core/io.js +90 -11
  115. package/dist/core/lock.js +6 -2
  116. package/dist/core/loops/brief-assembly.js +213 -0
  117. package/dist/core/loops/facade-schema.js +148 -0
  118. package/dist/core/loops/index.js +7 -0
  119. package/dist/core/loops/iteration-engine.js +139 -0
  120. package/dist/core/loops/lock.js +385 -0
  121. package/dist/core/loops/store.js +201 -0
  122. package/dist/core/loops/types.js +403 -0
  123. package/dist/core/loops/verbs.js +534 -0
  124. package/dist/core/markdown.js +15 -3
  125. package/dist/core/memory-compactor.js +432 -0
  126. package/dist/core/memory-git.js +152 -8
  127. package/dist/core/messaging.js +278 -0
  128. package/dist/core/migration.js +32 -1
  129. package/dist/core/mutation-pipeline.js +4 -2
  130. package/dist/core/operations/memory-mutation.js +129 -0
  131. package/dist/core/operations/memory-write.js +78 -0
  132. package/dist/core/operations/plan.js +190 -0
  133. package/dist/core/policy.js +169 -0
  134. package/dist/core/reputation.js +9 -3
  135. package/dist/core/schema.js +491 -6
  136. package/dist/core/search.js +21 -2
  137. package/dist/core/security-cache.js +71 -0
  138. package/dist/core/security-guard.js +152 -0
  139. package/dist/core/security-scoring.js +86 -0
  140. package/dist/core/sequence.js +130 -0
  141. package/dist/core/socket-client.js +113 -0
  142. package/dist/core/staleness.js +246 -0
  143. package/dist/core/state.js +98 -22
  144. package/dist/core/store-resolution.js +43 -11
  145. package/dist/core/toml-writer.js +76 -0
  146. package/dist/core/upgrades/backup.js +232 -0
  147. package/dist/core/upgrades/health-check.js +169 -0
  148. package/dist/core/upgrades/patches/candidate-archive.js +145 -0
  149. package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
  150. package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
  151. package/dist/core/upgrades/schema-version.js +97 -0
  152. package/dist/core/worktree.js +606 -0
  153. package/dist/facts.js +114 -0
  154. package/dist/facts.json +111 -0
  155. package/docs/architecture/project-refs.md +5 -1
  156. package/docs/cli.md +690 -43
  157. package/docs/concepts/ideation-loop.md +317 -0
  158. package/docs/concepts/loop-engine.md +456 -0
  159. package/docs/concepts/mcp-governance.md +268 -0
  160. package/docs/concepts/memory-staleness.md +122 -0
  161. package/docs/concepts/multi-agent-workflows.md +166 -0
  162. package/docs/concepts/plans-and-claims.md +31 -6
  163. package/docs/concepts/project-md-convention.md +35 -0
  164. package/docs/concepts/troubleshooting.md +220 -0
  165. package/docs/concepts/upgrade-cli.md +202 -0
  166. package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
  167. package/docs/context-format-changelog.md +2 -2
  168. package/docs/context-format.md +2 -2
  169. package/docs/index.md +68 -0
  170. package/docs/integrations/agents.md +15 -16
  171. package/docs/integrations/cline.md +88 -0
  172. package/docs/integrations/codex.md +75 -23
  173. package/docs/integrations/continue.md +60 -0
  174. package/docs/integrations/copilot.md +67 -9
  175. package/docs/integrations/kilocode.md +72 -0
  176. package/docs/integrations/mcp.md +304 -21
  177. package/docs/integrations/mistral-vibe.md +122 -0
  178. package/docs/integrations/opencode.md +84 -0
  179. package/docs/integrations/overview.md +23 -8
  180. package/docs/integrations/roo.md +74 -0
  181. package/docs/integrations/windsurf.md +83 -0
  182. package/docs/mcp-schema-changelog.md +191 -1
  183. package/docs/playbooks/integration/index.md +121 -0
  184. package/docs/playbooks/productivity/index.md +102 -0
  185. package/docs/playbooks/team/index.md +122 -0
  186. package/docs/product/agent-first-model.md +184 -0
  187. package/docs/product/entity-model-audit.md +462 -0
  188. package/docs/product/positioning.md +10 -10
  189. package/docs/quickstart-existing-project.md +135 -0
  190. package/docs/quickstart.md +124 -37
  191. package/docs/release-maintenance.md +79 -0
  192. package/docs/review.md +2 -0
  193. package/docs/server-operations.md +118 -0
  194. package/package.json +21 -13
  195. package/dist/commands/claude-desktop-extension.js +0 -18
  196. package/dist/commands/diff.js +0 -99
  197. package/dist/core/claude-desktop-extension.js +0 -224
@@ -0,0 +1,456 @@
1
+ # Loop engine
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.
9
+
10
+ ## Why
11
+
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.
16
+
17
+ A Loop captures the whole cycle as state:
18
+ *participants, phases, current position, artifacts, stop criteria*.
19
+ Agents read the loop, know exactly what phase we are in, and advance it.
20
+ The operator becomes optional in the hot path.
21
+
22
+ ## What a Loop is
23
+
24
+ A Loop is a **persistent thread of structured work** with:
25
+
26
+ - a **kind** (review, ideation, implementation, research, debug) that defines a protocol
27
+ - an ordered list of **phases**
28
+ - a set of **slots** — participant positions, each filled by an agent instance playing a role
29
+ - **artifacts** attached to phases (findings, syntheses, verdicts, …)
30
+ - optional **links** to existing brainclaw primitives (plans, sequences, claims, handoffs, candidates)
31
+ - a **stop condition** that determines when the loop auto-closes
32
+ - an **append-only event journal** for resume and debug
33
+
34
+ A Loop stores *references* to existing objects — it never duplicates them.
35
+ Claims, handoffs, and candidates remain the source of truth for their own data.
36
+
37
+ ## Data model
38
+
39
+ ```ts
40
+ type LoopId = `lop_${string}`;
41
+ type SlotId = `lsl_${string}`;
42
+
43
+ interface LoopThread {
44
+ schema_version: 1; // schema revision; bump on breaking changes
45
+ id: LoopId; // repo convention (not `loop_id`)
46
+ version: number; // monotonic; incremented on every mutation
47
+ mutation_id: string; // ULID of the last write; used for optimistic concurrency + idempotent retries
48
+
49
+ kind: LoopKind;
50
+ title: string;
51
+ goal?: string;
52
+ protocol?: LoopProtocolConfig; // persisted protocol knobs resolved at `open` time
53
+
54
+ status: LoopStatus;
55
+ phases: LoopPhase[]; // ordered; each phase carries its own advance policy
56
+ current_phase: string; // must match some phases[i].name
57
+ iteration_count: number; // incremented on re-entry into an earlier phase
58
+
59
+ slots: LoopSlot[];
60
+ artifacts: LoopArtifact[];
61
+ linked?: LoopLinks; // top-level context only (plan/sequence). Other refs live on artifacts/slots.
62
+ stop_condition?: StopCondition;
63
+
64
+ created_at: string; // ISO
65
+ updated_at: string;
66
+ closed_at?: string;
67
+ created_by: string; // agentId
68
+ }
69
+
70
+ type LoopStatus = 'open' | 'paused' | 'completed' | 'blocked' | 'cancelled';
71
+ type ReviewMode = 'asymmetric' | 'symmetric';
72
+
73
+ interface LoopProtocolConfig {
74
+ review_mode?: ReviewMode; // review loops persist their selected mode so resume/turn handlers are deterministic
75
+ }
76
+
77
+ interface LoopPhase {
78
+ name: string;
79
+ advance_when?: 'all' | 'any'; // default 'all' — every slot turn in this phase must be `done` before advance
80
+ }
81
+
82
+ interface LoopSlot {
83
+ slot_id: SlotId;
84
+ role: string; // e.g. "reviewer", "author", "challenger"
85
+ agent?: string; // agent type, e.g. "codex"
86
+ agent_id?: string; // specific registered agent id
87
+ assignment_id?: string; // set when a turn is dispatched
88
+ claim_id?: string; // for execution loops, the claim held by this slot
89
+ phase?: string; // which phase this slot currently participates in (supports parallel slots per phase)
90
+ status: 'open' | 'assigned' | 'working' | 'done';
91
+ }
92
+
93
+ interface LoopArtifact {
94
+ artifact_id: string;
95
+ phase: string;
96
+ type: string; // "finding" | "synthesis" | "verdict" | "plan_draft" | ...
97
+ ref?: LoopRef; // preferred: link to an existing primitive
98
+ body?: string; // inline content ≤ 4 KB; else force `ref`
99
+ produced_by?: SlotId;
100
+ produced_at: string;
101
+ }
102
+
103
+ type LoopRef =
104
+ | { kind: 'plan'; id: string }
105
+ | { kind: 'sequence'; id: string }
106
+ | { kind: 'claim'; id: string }
107
+ | { kind: 'handoff'; id: string }
108
+ | { kind: 'candidate'; id: string }
109
+ | { kind: 'message'; id: string };
110
+
111
+ // Top-level context only — handoff/claim/candidate refs belong on artifacts or slots.
112
+ interface LoopLinks {
113
+ plan_ids?: string[];
114
+ sequence_ids?: string[];
115
+ }
116
+
117
+ // StopCondition is composite: atomic clauses can be combined with any/all.
118
+ type AtomicStopCondition =
119
+ | { kind: 'phase_reached'; phase: string }
120
+ | { kind: 'reviewer_green' } // an `accepted` verdict artifact in any phase
121
+ | { kind: 'max_iterations'; n: number } // hard cap; on hit, close with status=blocked
122
+ | { kind: 'artifact_produced'; phase: string; type: string }
123
+ | { kind: 'manual' }; // only closes on explicit close
124
+
125
+ type StopCondition =
126
+ | AtomicStopCondition
127
+ | { kind: 'any'; conditions: StopCondition[] } // OR — any matching clause closes the loop
128
+ | { kind: 'all'; conditions: StopCondition[] }; // AND — every clause must match
129
+
130
+ // LoopEvent is a discriminated union with typed per-kind payloads (no loose `payload` map).
131
+ interface LoopEventBase {
132
+ event_id: string; // ULID
133
+ loop_id: LoopId;
134
+ seq: number; // monotonic per loop, starts at 1
135
+ at: string;
136
+ by?: string; // agentId or slot_id
137
+ mutation_id: string; // matches the thread.mutation_id written in the same 2-phase commit
138
+ }
139
+
140
+ type LoopEvent =
141
+ | (LoopEventBase & { kind: 'opened'; initial_phase: string; created_by: string })
142
+ | (LoopEventBase & { kind: 'phase_advanced'; from_phase: string; to_phase: string; iteration: number; reason?: string })
143
+ | (LoopEventBase & { kind: 'turn_assigned'; slot_id: SlotId; phase: string; assignment_id?: string; input?: string; retry_of?: string /* prior event_id */ })
144
+ | (LoopEventBase & { kind: 'turn_completed'; slot_id: SlotId; phase: string; artifact_id?: string; outcome: 'done' | 'failed' | 'cancelled'; failure_reason?: string })
145
+ | (LoopEventBase & { kind: 'artifact_added'; artifact_id: string; phase: string; type: string; produced_by?: SlotId })
146
+ | (LoopEventBase & { kind: 'linked'; target: LoopRef })
147
+ | (LoopEventBase & { kind: 'paused'; reason?: string })
148
+ | (LoopEventBase & { kind: 'resumed' })
149
+ | (LoopEventBase & { kind: 'closed'; final_status: Exclude<LoopStatus, 'open' | 'paused'>; reason?: string });
150
+
151
+ // Conflict records are NOT committed to the main journal — they do not carry `seq` and do
152
+ // not advance `thread.version`. They live in a separate observability log (`loops/conflicts/<id>.jsonl`)
153
+ // and are returned as-is in the error response of the rejected call.
154
+ interface LoopConflictRecord {
155
+ conflict_id: string; // ULID
156
+ loop_id: LoopId;
157
+ at: string;
158
+ attempted_by: string; // caller agentId
159
+ expected_version: number;
160
+ actual_version: number;
161
+ rejected_intent: string; // e.g. "advance" | "complete_turn"
162
+ client_request_id?: string;
163
+ }
164
+ ```
165
+
166
+ ## Lifecycle verbs
167
+
168
+ 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.
169
+
170
+ - **open** — create a new loop. Inserts `opened` event; `current_phase` set to `phases[0].name`.
171
+ - **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.
172
+ - **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.
173
+ - **close** — terminal: set `status` to `completed | cancelled | blocked` and `closed_at`. Inserts `closed`.
174
+
175
+ Two auxiliary verbs cover quality of life:
176
+
177
+ - **pause** / **resume** — suspend a loop without closing (e.g. waiting on an external input).
178
+ - **add_artifact** — attach an artifact to a phase without moving on.
179
+ - **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.
180
+
181
+ ## MCP facade: `bclaw_loop(intent)`
182
+
183
+ Consistent with `bclaw_work` and `bclaw_coordinate`: a single unified tool with an `intent` argument, a caller-identity envelope (`agent`, `agentId`), and a standard `FacadeResponse` envelope on the output.
184
+
185
+ ```ts
186
+ // Caller identity + idempotency envelope, consistent with bclaw_work / bclaw_coordinate.
187
+ interface BclawLoopCallerEnvelope {
188
+ agent?: string; // caller agent name
189
+ agentId?: string; // caller registered agent id
190
+ client_request_id?: string; // caller-minted ULID/UUIDv7 for idempotent retries (mutating intents only)
191
+ }
192
+
193
+ // Per-intent payloads. Every mutating intent supports `expected_version` + `client_request_id`.
194
+ type BclawLoopInput = BclawLoopCallerEnvelope & (
195
+ | { 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' */ }
196
+ | { intent: 'turn'; loop_id: LoopId; slot_id?: SlotId; role?: string; input?: string; dispatch?: boolean; expected_version?: number }
197
+ | { 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 }
198
+ | { intent: 'advance'; loop_id: LoopId; to_phase?: string; reason?: string; force?: boolean; expected_version?: number }
199
+ | { intent: 'add_artifact'; loop_id: LoopId; artifact: Omit<LoopArtifact, 'artifact_id' | 'produced_at'>; expected_version?: number }
200
+ | { intent: 'pause'; loop_id: LoopId; reason?: string; expected_version?: number }
201
+ | { intent: 'resume'; loop_id: LoopId; expected_version?: number }
202
+ | { intent: 'close'; loop_id: LoopId; status: 'completed' | 'cancelled' | 'blocked'; reason?: string; expected_version?: number }
203
+ | { intent: 'get'; loop_id: LoopId; include_events?: boolean }
204
+ | { intent: 'list'; kind?: LoopKind; status?: LoopStatus; linked_plan_id?: string; limit?: number; offset?: number }
205
+ );
206
+
207
+ // Standard facade envelope, matching bclaw_work / bclaw_coordinate output shape.
208
+ interface BclawLoopOutput {
209
+ status: 'ok' | 'error';
210
+ schema_version: string; // e.g. "0.6.0"
211
+ duration_ms?: number;
212
+ warnings?: string[];
213
+ artifacts?: Array<{ type: 'loop' | 'loop_event' | 'message'; id: string }>;
214
+ side_effects?: Array<{ action: 'create' | 'update'; entity: 'loop' | 'loop_event' | 'assignment'; id: string }>;
215
+ result: {
216
+ loop?: LoopThread; // single-loop intents
217
+ loops?: LoopThread[]; // list
218
+ events?: LoopEvent[]; // get with include_events
219
+ next_expected?: NextExpectedHint | null;
220
+ };
221
+ }
222
+
223
+ // Self-describing hint for the downstream agent: what intent to call next, with concrete ids.
224
+ type NextExpectedHint =
225
+ | { action: 'turn'; intent: 'bclaw_loop.turn'; phase: string; slot_id: SlotId; role: string; blocking_on: SlotId[] }
226
+ | { action: 'advance'; intent: 'bclaw_loop.advance'; from_phase: string; to_phase: string; blocking_on: SlotId[] }
227
+ | { action: 'close'; intent: 'bclaw_loop.close'; reason: string };
228
+ ```
229
+
230
+ **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).
231
+
232
+ **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.
233
+
234
+ **Concurrency control.** See the Persistence section below for the full lock-file CAS mechanism. In short: the server serializes all mutations on a given loop with an exclusive per-loop lock file, re-reads `thread.version` inside the lock, validates `expected_version` if supplied, and only then commits. Two racing writers cannot both succeed — one gets the updated version, the other gets a `409 conflict` with the observed `actual_version` to retry against. Conflict records live in a separate observability log and do not disturb the `seq`/`version` lockstep.
235
+
236
+ **Idempotency.** Mutating intents accept an optional `client_request_id` (caller-minted ULID/UUIDv7). The server caches the final response keyed on `(loop_id, client_request_id)` — or `(agent_id, client_request_id)` for `open`, which has no `loop_id` yet — alongside a `request_hash = sha256(canonical_json(request_without_caller_envelope))`. The idempotency lookup happens **inside the commit lock**, so concurrent retries serialize and see each other's cached result. If the same `client_request_id` arrives with a different `request_hash`, the call is rejected with `idempotency_key_reused_with_different_body` — callers must mint a fresh key for semantically different requests. Cache TTL is 24 h. The `mutation_id` inside the thread/event is server-minted and drives the 2-phase-commit replay story; it is orthogonal to caller idempotency.
237
+
238
+ > **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.
239
+
240
+ ## Default protocols
241
+
242
+ Each `kind` ships a default `phases[]` and `stop_condition`. Users can override either at `open` time.
243
+
244
+ | kind | phases | default stop_condition |
245
+ |---|---|---|
246
+ | `review` | `change_summary` → `findings` → `author_response` → `followup_review` → `verdict` | `reviewer_green` OR `max_iterations: 3` |
247
+ | `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 }` |
248
+ | `implementation` | `sequence_build` → `dispatch` → `execute` → `self_check` → `handoff_ready` | `artifact_produced { phase: handoff_ready, type: handoff }` |
249
+ | `research` / `debug` | user-defined | `manual` |
250
+
251
+ ## Relation to existing primitives
252
+
253
+ The Loop engine is a **control plane**; existing primitives remain the **data plane**.
254
+
255
+ | Primitive | Role in a loop |
256
+ |---|---|
257
+ | Plan | Often the output of an `ideation` loop; referenced from `linked.plan_ids` |
258
+ | Sequence | Compiled from a plan by an `implementation` loop |
259
+ | Claim | Scope lock held by an execution slot; pointed to from `slot.claim_id` |
260
+ | Handoff | Produced at `handoff_ready`; referenced as an artifact |
261
+ | Candidate | Reviewable artifact produced during implementation |
262
+ | Message | Human-readable turn content; can be referenced from artifacts |
263
+
264
+ 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.
265
+
266
+ ## Automation: extending `bclaw_coordinate(intent='review')`
267
+
268
+ This is the user-visible promise of the MVP — manual review round-trips disappear.
269
+
270
+ 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.
271
+
272
+ When `bclaw_coordinate(intent='review', open_loop: true)` is called, it:
273
+
274
+ 1. Creates the review candidate as today.
275
+ 2. Opens a `review` loop via `bclaw_loop(intent: 'open', kind: 'review', ...)` with slots `{role: 'author', agent: caller}`, `{role: 'reviewer', agent: target}`.
276
+ 3. Links the provided handoff/candidate to the loop as an artifact at `change_summary`.
277
+ 4. Advances to `findings` and calls `bclaw_loop(intent: 'turn')` to dispatch to the reviewer.
278
+ 5. On turn completion with a verdict artifact, auto-advances; `reviewer_green` stop closes.
279
+ 6. On non-green verdict with `iteration_count < max`, advances to `author_response`, dispatches to author.
280
+
281
+ ### Symmetric review-AND-fix mode
282
+
283
+ 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.
284
+
285
+ 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.
286
+
287
+ 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.
288
+
289
+ 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.
290
+
291
+ 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.
292
+
293
+ ## Automation: extending `bclaw_coordinate(intent='ideate')`
294
+
295
+ Shipped in v1.5.0 (pln#492). The full design — phases, context_filter,
296
+ iteration block, advance_gate, brief assembly, system events,
297
+ single-agent vs multi-agent UX — lives in [ideation-loop.md](./ideation-loop.md).
298
+ Summary for the loop-engine perspective:
299
+
300
+ - `bclaw_coordinate(intent='ideate', task=…, [targetAgents=[…]])` opens
301
+ an ideation_loop with the caller as `champion` slot and the targets
302
+ (when provided) as `critic` slots. The task is stored verbatim as
303
+ the `proposal` artifact (sliced to the 4 KB body cap).
304
+ - Single-agent mode (no `targetAgents`): the loop opens at the
305
+ proposal phase and stops there. The champion drives the cycle
306
+ manually via `bclaw_loop(intent='turn'|'advance')`. Useful when the
307
+ loop's structure (memory filter, gate, iteration accounting) is
308
+ what's wanted, not the multi-slot orchestration.
309
+ - Multi-agent mode (explicit `targetAgents`): the driver advances
310
+ proposal → critique and dispatches a turn per critic with a brief
311
+ assembled by `buildIdeationBrief` — context-filtered (critic sees
312
+ only `traps + feedback + runtime_notes + critique_history`),
313
+ BM25-ranked via `search()`, capped at 48 KB.
314
+
315
+ The ideation_loop introduces three loop-engine extensions consumed by
316
+ this driver:
317
+
318
+ - `LoopPhase.context_filter?: LoopContextCategory[]` — closed enum
319
+ with `'*'` wildcard. Drives per-phase memory selection at brief
320
+ assembly time.
321
+ - `LoopPhase.advance_gate?: StopCondition` — re-uses the StopCondition
322
+ vocabulary as a phase-exit guard. When unmet, the driver emits a
323
+ `phase_advance_blocked` system event (a non-artifact event in the
324
+ journal) with a structured `gate_reason` and throws an actionable
325
+ error. The default ideation `critique` advance_gate is
326
+ `min_artifacts_by_type { type: 'critique', n: 3, scope: 'phase' }`.
327
+ - `LoopProtocolConfig.iteration?: { cycle, max_iterations, exit_when }`
328
+ — wraps the inner critique↔revision loop. The FSM
329
+ (`decideNextPhase` in `iteration-engine.ts`) handles cycle progress,
330
+ exit_when predicates (`no_new_critique_artifacts` / `critic_signal`),
331
+ and emits `max_iterations_reached` when the cap fires.
332
+
333
+ Both new event kinds — `phase_advance_blocked` and
334
+ `max_iterations_reached` — live in the same event journal as
335
+ `turn_assigned` / `phase_advanced`. They are intentionally **not**
336
+ artifacts (which would force every consumer to filter `is_system`
337
+ before processing content).
338
+
339
+ ## Persistence
340
+
341
+ ```
342
+ .brainclaw/loops/
343
+ threads/<id>.json # main state
344
+ events/<id>.jsonl # append-only journal (seq/version authoritative)
345
+ locks/<id>.lock # per-loop exclusive lock (all intents on an existing loop, and opt-out `open`)
346
+ locks/open/<agent_id>/<client_request_id>.lock # idempotent-`open` lock keyed on idempotency scope
347
+ idempotency/<id>/<client_request_id>.json # 24h cache of completed mutation responses (one loop)
348
+ idempotency-open/<agent_id>/<client_request_id>.json # 24h cache for `open` intent (no loop_id yet)
349
+ conflicts/<id>.jsonl # observability-only log of rejected CAS attempts
350
+ ```
351
+
352
+ **Lock scoping.** Two lock-path families exist:
353
+
354
+ - `locks/<loop_id>.lock` — used by every mutation on an existing loop (`turn`, `advance`, `complete_turn`, `add_artifact`, `pause`, `resume`, `close`), **and** by `open` when the caller does not supply `client_request_id`. In the opt-out `open` case, the `loop_id` (ULID) is minted by the handler **before** step 1 and reused as the lock key; since nothing else can observe this id yet, there is no race between concurrent opt-out calls.
355
+ - `locks/open/<agent_id>/<client_request_id>.lock` — used by `open` when the caller supplies `client_request_id`. The lock is keyed on the idempotency scope, not on a `loop_id`. Concurrent retries of the same `open` request serialize on this path before any id is minted. The real `loop_id` is minted inside the lock at step 3 and persisted into the idempotency record so retries return the same id.
356
+
357
+ **Lock file contents.** Every lock file is a small JSON blob, not an empty marker:
358
+
359
+ ```json
360
+ {
361
+ "pid": 12345,
362
+ "host_id": "frams99l000391",
363
+ "agent_id": "agt_…",
364
+ "acquired_at": "2026-04-17T06:30:12.000Z",
365
+ "lease_until": "2026-04-17T06:31:12.000Z",
366
+ "hard_deadline": "2026-04-17T06:35:12.000Z",
367
+ "mutation_id": "01HZ…"
368
+ }
369
+ ```
370
+
371
+ **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.
372
+
373
+ Default `max_mutation_duration` per intent:
374
+
375
+ | Intent | `max_mutation_duration` | Rationale |
376
+ |---|---|---|
377
+ | `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. |
378
+ | `add_artifact`, `complete_turn` | 60 s | May write small external ref files. |
379
+
380
+ 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.
381
+
382
+ **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.
383
+
384
+ **Commit protocol (lock-file CAS with intra-lock idempotency):**
385
+
386
+ 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.
387
+
388
+ 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`).
389
+ 2. **Idempotency short-circuit (inside lock).** If the caller supplied `client_request_id`:
390
+ - For mutations on an existing loop: look up `idempotency/<id>/<client_request_id>.json`.
391
+ - For `open`: look up `idempotency-open/<agent_id>/<client_request_id>.json`.
392
+ - If found, verify the stored `request_hash` matches `sha256(canonical_json(request_without_caller_envelope))`. On match, release the lock and return the cached response. On mismatch, return `{ status: 'error', code: 'idempotency_key_reused_with_different_body', stored_hash, submitted_hash }`.
393
+ 3. **Replay / auth / CAS check / id minting.**
394
+ - For mutations on an existing loop: read the current `thread.json`, then inspect `events/<id>.jsonl`. If `max(event.seq) > thread.version`, first replay the missing journal entries into the materialized thread and rewrite `thread.json` so `thread.version = max(event.seq)` before evaluating any new mutation. This replay-before-CAS step is mandatory: the next mutation always starts from the latest journal-authoritative state, never from a stale materialized thread.
395
+ - For slot-bound intents (`complete_turn` today): resolve the target slot from that up-to-date thread and verify `caller.agentId === slot.agent_id` or `caller.agentId === thread.created_by`. Otherwise, release the lock and return `{ status: 'error', code: 'unauthorized_slot_write' }`.
396
+ - 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 }`.
397
+ - 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.
398
+ - For opt-out `open`: `loop_id` was already minted before step 1; nothing to do here.
399
+ 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.
400
+ 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.
401
+ 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.)
402
+ 7. **Release lock.** Stop the lease-renewal heartbeat and remove the lock file.
403
+
404
+ **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.
405
+
406
+ 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.
407
+
408
+ **Stale-lock recovery (owner-liveness + deadline, not age-based):**
409
+
410
+ - Read the lock blob. If `now > hard_deadline` → the mutation exceeded its intent-specific cap → remove the lock regardless of liveness.
411
+ - 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.
412
+ - Else if `now > lease_until + grace` (default grace = 30 s) and the owner has not renewed, treat as abandoned → remove the lock.
413
+ - Else the lock is considered live; callers keep retrying.
414
+
415
+ 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.
416
+
417
+ **Journal crash recovery:**
418
+
419
+ - If `max(event.seq) > thread.version` → the journal has events past the last materialized state. Replay them to rebuild `thread.json`, then rewrite it with the final `mutation_id`. This is not just a background repair path: step 3 above must do this replay synchronously before the next mutation proceeds.
420
+ - If `max(event.seq) < thread.version` → impossible under the protocol above; surface a diagnostic (corrupted journal).
421
+ - If `max(event.seq) === thread.version` but `mutation_id` differs → crash mid-commit (temp file written, rename not flushed). Re-materialize from the journal's last event.
422
+
423
+ **GC:** closed loops older than N days are archived into `.brainclaw/gc-backups/loops/` alongside plans and handoffs. Idempotency records older than 24 h, stale lock files, and conflict logs older than 7 d are swept at the same time.
424
+
425
+ ## Routing and multi-instance
426
+
427
+ - Discussion loops (`review`, `ideation`) route by `slot_id` — the engine writes to the slot's agent inbox via the existing coordinate path.
428
+ - Execution loops (`implementation`) route by `claim_id` — preserved from the claim-routed model already in use.
429
+ - `session_id` is not a routing key; it remains observability-only. This is consistent with `architecture_session_centric_identity` in memory.
430
+
431
+ ## Open questions (resolved / deferred)
432
+
433
+ Status after Codex schema review (cnd#574 / `dec_be66ccbf`, verdict `needs_revision` → addressed in v8):
434
+
435
+ 1. **Custom phases per loop** — **Resolved: allow with validation.** `open` accepts arbitrary `LoopPhase[]` (non-empty, unique `name` values, at least one phase must be reachable from `phases[0]`). Built-in protocols still ship with defaults.
436
+ 2. **Parallel slots in a single phase** — **Resolved: per-phase `advance_when`.** Each `LoopPhase` carries an optional `advance_when: 'all' | 'any'` (default `'all'`). `advance` blocks until the policy is satisfied by the slots participating in the current phase.
437
+ 3. **Cross-project loops** — **Deferred to phase 2.** MVP is single-project. Tracked alongside `pln_12d33efe` (cross-project coordinate).
438
+ 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.
439
+ 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.
440
+
441
+ ## Next steps
442
+
443
+ 1. If this final v8 review is green, lock the schema (this doc → `types.ts` in `src/core/loops/`).
444
+ 2. Implement the four verbs (`open`, `turn`, `advance`, `close`) with the 2-phase-commit persistence described above.
445
+ 3. Wire `bclaw_loop` into the MCP surface (pending pln#392 versioning policy).
446
+ 4. Build the `review` protocol end-to-end (pln#395) as the first user-visible loop.
447
+ 5. Add the `open_loop` opt-in on the existing `bclaw_coordinate(intent='review')` — the first manual-process killer.
448
+
449
+ ## Related
450
+
451
+ - [plans-and-claims.md](plans-and-claims.md)
452
+ - [coordination.md](coordination.md)
453
+ - [runtime-notes.md](runtime-notes.md)
454
+ - pln#394 `feat/loop-engine-mvp`
455
+ - pln#395 `feat/review-loop-protocol`
456
+ - pln#392 `doc/mcp-versioning-and-surface-governance` (prerequisite)