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.
- package/README.md +41 -12
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-coordination.js +65 -1
- package/dist/commands/attempt-authority.js +80 -0
- package/dist/commands/harvest.js +140 -61
- package/dist/commands/loop.js +34 -0
- package/dist/commands/loops-handlers.js +87 -14
- package/dist/commands/mcp-catalog.js +42 -18
- package/dist/commands/mcp-schemas.generated.js +44 -0
- package/dist/commands/mcp-write-claims.js +128 -1
- package/dist/commands/mcp-write-coordination.js +146 -76
- package/dist/core/agent-capability.js +1 -1
- package/dist/core/agent-files.js +21 -21
- package/dist/core/agentrun-reconciler.js +148 -22
- package/dist/core/agentruns.js +254 -29
- package/dist/core/assignment-request-schema.js +7 -0
- package/dist/core/assignment-sweeper.js +5 -3
- package/dist/core/assignments.js +131 -33
- package/dist/core/claim-request-schema.js +7 -0
- package/dist/core/claims.js +53 -2
- package/dist/core/dispatch-status.js +16 -6
- package/dist/core/dispatcher.js +51 -51
- package/dist/core/entity-operations.js +20 -0
- package/dist/core/events.js +4 -0
- package/dist/core/execution-adapters.js +160 -14
- package/dist/core/execution-contract.js +345 -0
- package/dist/core/execution.js +130 -16
- package/dist/core/harness-adapters/base.js +150 -0
- package/dist/core/harness-adapters/claude.js +39 -0
- package/dist/core/harness-adapters/codex.js +57 -0
- package/dist/core/harness-adapters/harvest.js +109 -0
- package/dist/core/harness-adapters/index.js +8 -0
- package/dist/core/harness-adapters/prompt-only.js +13 -0
- package/dist/core/harness-adapters/registry.js +48 -0
- package/dist/core/harness-adapters/result.js +33 -0
- package/dist/core/harness-adapters/types.js +2 -0
- package/dist/core/ideation-loop-close.js +25 -2
- package/dist/core/instruction-templates.js +3 -2
- package/dist/core/loop-turn-dispatch.js +207 -0
- package/dist/core/loops/artifact-contract.js +11 -0
- package/dist/core/loops/attempt-authority.js +476 -0
- package/dist/core/loops/attempt-generations.js +509 -0
- package/dist/core/loops/attempt-reservation.js +197 -35
- package/dist/core/loops/attempt-rollout.js +404 -0
- package/dist/core/loops/attempt-takeover.js +155 -0
- package/dist/core/loops/bootstrap-acquire.js +7 -3
- package/dist/core/loops/evidence.js +187 -0
- package/dist/core/loops/facade-schema.js +41 -10
- package/dist/core/loops/gate-policy.js +485 -0
- package/dist/core/loops/impl-bind.js +37 -79
- package/dist/core/loops/index.js +9 -0
- package/dist/core/loops/iteration-engine.js +31 -19
- package/dist/core/loops/kind-policies.js +90 -0
- package/dist/core/loops/lock.js +71 -13
- package/dist/core/loops/reconcile-turn.js +235 -18
- package/dist/core/loops/result-reducers.js +99 -10
- package/dist/core/loops/store.js +30 -3
- package/dist/core/loops/turn-execution.js +480 -0
- package/dist/core/loops/types.js +113 -2
- package/dist/core/loops/verbs.js +332 -99
- package/dist/core/loops/verify-command.js +31 -8
- package/dist/core/loops/workspace-digest.js +54 -0
- package/dist/core/protocol-tool-policy.js +44 -0
- package/dist/core/review-loop-close.js +25 -3
- package/dist/core/review-loop-turn-dispatch.js +210 -161
- package/dist/core/runtime-signals.js +62 -25
- package/dist/core/schema.js +35 -0
- package/dist/core/spawn-check.js +3 -2
- package/dist/core/upgrades/backup.js +27 -4
- package/dist/facts.js +9 -8
- package/dist/facts.json +8 -7
- package/docs/PROTOCOL.md +6 -4
- package/docs/cli.md +49 -1
- package/docs/concepts/attempt-authority.md +407 -0
- package/docs/concepts/evidence-attestations.md +135 -0
- package/docs/concepts/execution-contract.md +166 -0
- package/docs/concepts/harness-adapters.md +166 -0
- package/docs/concepts/ideation-loop.md +5 -4
- package/docs/concepts/loop-engine.md +348 -133
- package/docs/index.md +4 -1
- package/docs/integrations/codex.md +3 -3
- package/docs/integrations/hermes.md +42 -3
- package/docs/integrations/mcp.md +75 -9
- package/docs/loops/debug.md +144 -0
- package/docs/loops/ideation.md +158 -0
- package/docs/loops/implementation.md +154 -0
- package/docs/loops/research.md +136 -0
- package/docs/loops/review.md +200 -0
- package/docs/mcp-schema-changelog.md +14 -5
- package/docs/product/agent-first-model.md +33 -33
- package/package.json +1 -1
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
# Loop engine
|
|
2
2
|
|
|
3
|
-
brainclaw coordinates many agents against shared state.
|
|
4
|
-
|
|
5
|
-
—
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Status:
|
|
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
|
-
|
|
13
|
-
an operator
|
|
14
|
-
|
|
15
|
-
|
|
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;
|
|
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?:
|
|
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
|
|
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.
|
|
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
|
-
|
|
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;
|
|
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?:
|
|
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:
|
|
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;
|
|
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
|
-
##
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
|
303
|
-
|
|
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
|
-
##
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
-
|
|
380
|
-
vocabulary as a phase-exit guard. When unmet, the driver
|
|
381
|
-
`phase_advance_blocked` system event
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
(`
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
**
|
|
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 |
|
|
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
|
-
|
|
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
|
|
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.**
|
|
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. **
|
|
458
|
-
|
|
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.**
|
|
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 —
|
|
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 (
|
|
679
|
+
**Stale-lock recovery (proof-based and generation-fenced):**
|
|
467
680
|
|
|
468
|
-
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
-
|
|
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
|
-
|
|
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
|
-
-
|
|
486
|
-
- Execution loops (`implementation`) route by `claim_id
|
|
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
|
-
##
|
|
743
|
+
## Implementation status
|
|
523
744
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|