brainclaw 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +585 -499
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/commands/harvest.js +1 -1
  4. package/dist/commands/hooks.js +73 -73
  5. package/dist/commands/init.js +1 -1
  6. package/dist/commands/install-hooks.js +78 -78
  7. package/dist/commands/mcp-read-handlers.js +57 -14
  8. package/dist/commands/mcp.js +79 -13
  9. package/dist/commands/switch.js +26 -5
  10. package/dist/commands/version.js +1 -1
  11. package/dist/core/agent-capability.js +19 -4
  12. package/dist/core/agent-files.js +119 -119
  13. package/dist/core/codev-prompts.js +38 -38
  14. package/dist/core/default-profiles/doctor.yaml +11 -11
  15. package/dist/core/default-profiles/janitor.yaml +11 -11
  16. package/dist/core/default-profiles/onboarder.yaml +11 -11
  17. package/dist/core/default-profiles/reviewer.yaml +13 -13
  18. package/dist/core/dispatcher.js +1 -1
  19. package/dist/core/entity-operations.js +29 -3
  20. package/dist/core/execution.js +1 -1
  21. package/dist/core/loops/verbs.js +0 -1
  22. package/dist/core/messaging.js +2 -2
  23. package/dist/core/protocol-skills.js +164 -164
  24. package/dist/core/runtime-signals.js +1 -1
  25. package/dist/core/search.js +19 -2
  26. package/dist/core/security-guard.js +207 -207
  27. package/dist/core/spawn-check.js +16 -2
  28. package/dist/core/staleness.js +1 -1
  29. package/dist/core/store-resolution.js +26 -7
  30. package/dist/core/worktree.js +18 -18
  31. package/dist/facts.js +3 -3
  32. package/dist/facts.json +2 -2
  33. package/docs/PROTOCOL.md +1 -1
  34. package/docs/adapters/openclaw.md +43 -43
  35. package/docs/architecture/project-refs.md +328 -328
  36. package/docs/cli.md +2093 -2093
  37. package/docs/concepts/coordination.md +52 -52
  38. package/docs/concepts/coordinator-runbook.md +129 -129
  39. package/docs/concepts/dispatch-lifecycle.md +245 -245
  40. package/docs/concepts/event-log-store.md +928 -928
  41. package/docs/concepts/ideation-loop.md +317 -317
  42. package/docs/concepts/loop-engine.md +520 -511
  43. package/docs/concepts/mcp-governance.md +268 -268
  44. package/docs/concepts/memory.md +84 -84
  45. package/docs/concepts/multi-agent-workflows.md +167 -167
  46. package/docs/concepts/observer-protocol.md +361 -361
  47. package/docs/concepts/plans-and-claims.md +217 -217
  48. package/docs/concepts/project-md-convention.md +35 -35
  49. package/docs/concepts/runtime-notes.md +38 -38
  50. package/docs/concepts/troubleshooting.md +254 -254
  51. package/docs/concepts/workspace-bootstrapping.md +142 -142
  52. package/docs/context-format-changelog.md +35 -35
  53. package/docs/context-format.md +48 -48
  54. package/docs/index.md +65 -65
  55. package/docs/integrations/agents.md +158 -158
  56. package/docs/integrations/claude-code.md +23 -23
  57. package/docs/integrations/cline.md +77 -77
  58. package/docs/integrations/continue.md +55 -55
  59. package/docs/integrations/copilot.md +68 -68
  60. package/docs/integrations/cursor.md +23 -23
  61. package/docs/integrations/kilocode.md +72 -72
  62. package/docs/integrations/mcp.md +377 -377
  63. package/docs/integrations/mistral-vibe.md +122 -122
  64. package/docs/integrations/openclaw.md +92 -92
  65. package/docs/integrations/opencode.md +84 -84
  66. package/docs/integrations/overview.md +115 -115
  67. package/docs/integrations/roo.md +71 -71
  68. package/docs/integrations/windsurf.md +77 -77
  69. package/docs/mcp-schema-changelog.md +360 -356
  70. package/docs/playbooks/integration/index.md +121 -121
  71. package/docs/playbooks/orchestration.md +37 -0
  72. package/docs/playbooks/productivity/index.md +99 -99
  73. package/docs/playbooks/team/index.md +117 -117
  74. package/docs/product/agent-first-model.md +184 -184
  75. package/docs/product/entity-model-audit.md +462 -462
  76. package/docs/product/positioning.md +86 -86
  77. package/docs/quickstart-existing-project.md +107 -107
  78. package/docs/quickstart.md +183 -183
  79. package/docs/release-maintenance.md +79 -79
  80. package/docs/reputation.md +52 -52
  81. package/docs/review.md +45 -45
  82. package/docs/security.md +212 -212
  83. package/docs/server-operations.md +118 -118
  84. package/docs/storage.md +106 -106
  85. package/package.json +80 -65
  86. package/docs/concepts/event-log-store-critique-A.md +0 -333
  87. package/docs/concepts/event-log-store-critique-B.md +0 -353
  88. package/docs/concepts/event-log-store-phase0-measurements.md +0 -58
  89. package/docs/concepts/event-log-store-proposal-A.md +0 -365
  90. package/docs/concepts/event-log-store-proposal-B.md +0 -404
  91. package/docs/concepts/identity-model-proposal.md +0 -371
@@ -1,361 +1,361 @@
1
- # Observer Protocol — language-agnostic read-only surfaces
2
-
3
- Status: spec (pln#560 step 1). Pivot deliverable: it serves the VS Code
4
- extension, the JetBrains plugin, and any future surface identically. Companion
5
- to `event-log-store.md` (the journal this protocol consumes) and the VS Code
6
- vision §5 (the UX it powers).
7
-
8
- ## 1. The one rule
9
-
10
- **An observability surface is a pure consumer of the event journal. It never
11
- acquires a store lock, never writes inside `.brainclaw/`, never runs a polling
12
- timer against the MCP server for display.** It tails the append-only journal,
13
- projects board state in memory, and refreshes the affected section when a new
14
- record arrives. The MCP server is reserved for *actions* (accept / release /
15
- dispatch / transition) through a separate, lazily-created client.
16
-
17
- Why this exists (2026-06-10 calibration): the prior extension "read" the board
18
- by calling a path that mutated and git-committed the entire store under the
19
- lock (`autoAcknowledge → persistState`, held >5s), ran the agent-run reconciler
20
- twice per poll with locked writes, created ~120 locked "unverified" event files
21
- per hour per run, and impersonated the parent shell's agent identity —
22
- consuming that agent's cursor. A dashboard is not an agent. This protocol makes
23
- that class of bug unrepresentable: a conforming observer *cannot* write.
24
-
25
- ## 2. What the observer reads
26
-
27
- The journal defined in `event-log-store.md`:
28
-
29
- ```
30
- .brainclaw/events/
31
- meta.json # { next_seq, active_segment, entity_revs } — a cache
32
- seg-<firstSeq>.jsonl # immutable once rolled; named by first seq it holds
33
- seg-<firstSeq>.jsonl # active = lexicographically-last segment
34
- checkpoints/ # NOT YET EMITTED by the writer (see §5) — directory
35
- ckpt-<seq>.json # may be absent today; self-contained state manifest
36
- ```
37
-
38
- **Segment naming is normative.** `firstSeq` is encoded as a **decimal,
39
- zero-padded to 8 digits** (e.g. `seg-00018342.jsonl`) so directory-listing
40
- lex-sort matches numeric seq order with no parsing. Implementations MUST emit
41
- and accept exactly this format; any other padding (or none) breaks the
42
- binary-search-by-filename in §5. The 8-digit field overflows at `seq > 1e8`;
43
- at the historical write rate (~17k events to date) this is decades away, but
44
- a journal that crosses the boundary requires a coordinated widen-and-pad
45
- migration in writer + every observer (deferred until needed; flagged here so
46
- no implementer assumes the pad width is incidental).
47
-
48
- `meta.json` is advisory only (§4): the observer reads it to cheaply detect
49
- "did anything change" but never trusts its `entity_revs` or `next_seq` for
50
- correctness — those are derived from the journal tail itself.
51
-
52
- Record envelope (v2), one JSON object per line:
53
-
54
- ```jsonc
55
- { "v": 2, "seq": 18342, "ts": "…", "writer": "w_…", "agent": "claude-code",
56
- "action": "update", "item_type": "plan", "item_id": "pln_…",
57
- "entity_rev": 7, "summary": "…", "payload": { /* full post-image */ } }
58
- ```
59
-
60
- Action → class mapping (the observer needs the class, not a hardcoded verb
61
- list — it is `ACTION_CLASS_BY_ACTION` in `event-log.ts`, and MUST be mirrored
62
- in every implementation or fetched from a shared manifest):
63
-
64
- | Class | Effect on the projection |
65
- |---|---|
66
- | `entity-state` (`create`,`update`,`accept`,`reject`,`claim`,`release_claim`,`rollback`,`upgrade`,`backfill`) | upsert `payload` at `(item_type,item_id)` |
67
- | `tombstone` (`delete`) | remove `(item_type,item_id)` |
68
- | `journal-meta` (`checkpoint_ref`,`journal_note`,`seq_repair`,`federation_apply`) | ignore for state; `checkpoint_ref` is a bootstrap hint (§5) |
69
- | `observability` (`session_start`,`session_end`,`assignment_offered`,`assignment_progress`,`run_progress`) | activity feed only — never a state upsert |
70
- | `registry-lifecycle` (`assignment_*`,`run_*`) | upsert when `payload` present (phase 1.5+), else a status/activity signal |
71
-
72
- The observer is **forward-compatible**: an unknown `action` whose class it
73
- cannot resolve is applied as `entity-state` iff it carries a `payload` and an
74
- `item_id`, else treated as an activity signal. Never crash on an unknown verb.
75
-
76
- ## 3. The cursor lives OUTSIDE the store
77
-
78
- The observer's read position is a **seq watermark** persisted in *client*
79
- storage, never in `.brainclaw/`:
80
-
81
- - VS Code: `ExtensionContext.workspaceState`, key `bclaw.observer.cursor.<project_id>`.
82
- - JetBrains: `PropertiesComponent` / project-scoped state, same key shape.
83
- - Generic: any client-private kv keyed by `project_id`.
84
-
85
- Shape: `{ seq: number, checkpoint_seq: number }`. `seq` = highest record seq
86
- applied; `checkpoint_seq` = the checkpoint the in-memory projection was last
87
- seeded from (for fast re-bootstrap). The store's own `.cursors/` directory is
88
- the AGENTS' read position and is **off-limits** to observers — touching it is
89
- the identity-leak bug this protocol forbids.
90
-
91
- Rationale: a watermark survives segment rotation, compaction, and archival
92
- (byte offsets do not). It is private to the surface, so N observers never
93
- interfere with each other or with agents.
94
-
95
- ## 4. Change detection — a file watch, not a poll, not a lock
96
-
97
- The observer watches the journal directory for growth and reacts:
98
-
99
- 1. Watch `.brainclaw/events/` (the active segment's size/mtime, and creation of
100
- new `seg-*.jsonl`). VS Code: `FileSystemWatcher` on `events/seg-*.jsonl` +
101
- `meta.json`. JetBrains: `VirtualFileListener` / NIO `WatchService`. Generic
102
- fallback: stat the active segment on a *long* interval (≥10 s) — this is a
103
- stat, not an MCP call, and acquires no lock.
104
- 2. On a growth signal, **tail forward** from `cursor.seq` (§5) and apply records
105
- to the in-memory projection.
106
- 3. `meta.json` is advisory only; never trust it for correctness — the journal
107
- tail is the truth (it may be a stale cache mid-write). Use it only to detect
108
- "did anything change" cheaply.
109
-
110
- There is no MCP server process for display. The watcher is OS-level; the read
111
- is a file read. Under the 2026-06-10 load (3 workers + open surface) this yields
112
- zero lock acquisitions by the surface — the validation gate (step 3).
113
-
114
- ## 5. Bootstrap and tail algorithm
115
-
116
- **Status of checkpoint emission (2026-06-12, pln#543 step 4 landed):** the
117
- writer does NOT yet produce `checkpoints/ckpt-*.json` files — checkpoint
118
- emission ships with step 3/5 of pln#543. Until then, the empty-seed + full
119
- tail path below is the **primary** cold-start path in production, not a
120
- degenerate fallback. The checkpoint-first path is the spec the consumer must
121
- implement for forward-compatibility; an observer that hard-requires a
122
- checkpoint at activation is broken against today's store. The perf targets
123
- in §10 assume checkpoint emission; until it ships, "activation → first
124
- summary" is bounded by the full tail length instead (~10 MB of segments in
125
- the typical case, sub-second in practice, but the budget no longer has
126
- slack).
127
-
128
- **Cold start (no cursor, or cursor below the oldest live segment):**
129
-
130
- 1. **If a verified checkpoint exists**, load the newest `ckpt-<S>.json` →
131
- seed the in-memory projection (full post-image set at head `S`). Set
132
- `cursor.checkpoint_seq = S`. (Today: this branch is dead until the
133
- writer ships checkpoint emission.)
134
- 2. **Otherwise** (today's primary path), seed from the empty projection
135
- with `cursor.checkpoint_seq = 0`.
136
- 3. Tail every record with `seq > checkpoint_seq` across segments in
137
- (segment, file-line) order; apply by class (§2). With no checkpoint
138
- this is a full replay from seq 1, bounded by retention (the
139
- `events/archive/` floor — segments are park-don't-deleted past the
140
- second-newest verified checkpoint; with no checkpoint, no segment is
141
- ever eligible for archive, so "the journal" = "every segment ever
142
- written" until checkpoint emission ships).
143
- 4. Set `cursor.seq` to the last applied seq. Render.
144
-
145
- If the cursor's `seq` is **below the oldest non-archived segment's first
146
- seq** (gap — segments archived past the watermark), discard the cursor and
147
- cold-start: notifications degrade, state never does. Today no segment is
148
- ever archived (gc requires a verified checkpoint floor, see §2.3 of the
149
- store spec), so this branch is unreachable in production until checkpoint
150
- emission ships — but the rule is normative regardless: an observer that
151
- crashes on a gap is broken against any future store.
152
-
153
- **Warm tail (cursor present, within live segments):**
154
-
155
- 1. Binary-search the segment whose name (`seg-<firstSeq>`) contains
156
- `cursor.seq + 1` (filenames sort by first seq).
157
- 2. Stream forward from that point across segments; apply by class.
158
- 3. A **torn tail** (final line unparseable or missing trailing `\n`) is expected
159
- crash residue mid-write by an agent — skip it; it reappears complete on the
160
- next growth signal. Never block on it.
161
- 4. A mid-file unparseable line is logged and skipped (do not halt the tail).
162
- 5. Advance `cursor.seq` only over records actually applied.
163
-
164
- Replay order is always (segment order, then file-line order) — never sorted by
165
- seq (matches the store's own reducer; a dup `seq` from a lock-steal applies
166
- later-line-wins, harmlessly, in a read-only projection).
167
-
168
- ## 6. Board projection — which records touch which section
169
-
170
- The in-memory projection is `Map<item_type, Map<item_id, payload>>` plus a
171
- bounded recent-activity ring (observability + registry signals, last N). The
172
- board sections are derived; a record invalidates only the sections its
173
- `item_type` feeds, and only those re-render (push-by-affected-section, §5.3):
174
-
175
- | `item_type` | Invalidates sections |
176
- |---|---|
177
- | `plan` | IN_PROGRESS, SPRINTS, BACKLOG, ATTENTION (badge), SYSTEM (counts) |
178
- | `claim` | IN_PROGRESS, AGENTS (roster freshness) |
179
- | `assignment` | IN_PROGRESS, ATTENTION (blocked/failed), "Recently terminal" |
180
- | `agent_run` | IN_PROGRESS (worker rows), AGENTS, "Recently terminal" |
181
- | `candidate` | ATTENTION (human-review), CANDIDATES |
182
- | `action` | ATTENTION (the dominant attention input) |
183
- | `constraint`/`decision`/`trap` | SYSTEM (counts), TRAPS |
184
- | `handoff` | ACTIVITY, SYSTEM (counts) |
185
- | `sequence` | SPRINTS |
186
- | `session`/`*_progress` (observability) | ACTIVITY feed only — never a section state change |
187
-
188
- `attention_required` is computed by the observer from the projection (actions +
189
- human candidates + blocked/failed assignments + failed runs + evidence-
190
- contradicted terminals), matching what the server-side composite returns — the
191
- surface must not under-count by reading "actions only" (the pln#559 fix, now in
192
- the projection rule).
193
-
194
- ### 6.1 Dual-mode coverage gap (CLOSED by pln#568 phase 1.5)
195
-
196
- > **Status (pln#568):** the writer-side gap below is **closed**. The
197
- > registry / coordination families (claim, assignment, agent_run,
198
- > action_required [journaled under item_type `state`], candidate, sequence,
199
- > and SHARED runtime_note) now emit full entity-state **post-images** on their
200
- > persist chokepoint (`src/core/events/registry-post-image.ts`), and the
201
- > observer materializer projects them (`board-projection.ts` ARRAY_SLOT).
202
- >
203
- > **Cutover signal (O2, resolved):** an observer switches a registry family
204
- > from the MCP `board_summary` seed to the journal only once the journal
205
- > carries the `journal_note` kind **`registry_genesis`** marker — emitted by
206
- > `runRegistryGenesisSupplement` (run via `brainclaw migrate --enable-journal`)
207
- > after it backfills every pre-existing registry entity. The marker is the
208
- > safety gate: without a complete backfill a partially-journaled store would
209
- > undercount the attention badge (trp#559). `BoardObserver.registryAuthoritative()`
210
- > tracks the marker (sticky, re-derived on cold start by replaying from the
211
- > checkpoint floor); `mergeCounts(journal, seed, journalActive, registryAuthoritative)`
212
- > takes claims/assignments/runs/actions from the journal when it is set, and
213
- > from the seed otherwise. `agents`/`sessions` are never journaled → always seed.
214
- > A store that has NOT run the supplement keeps the seed (no regression).
215
- >
216
- > The historical (pre-pln#568) description below is kept for context.
217
-
218
- The journal classifies records into five classes (§2). In phase 1 / `dual`
219
- mode — what runs today after pln#543 step 4 — **registry-lifecycle records
220
- are payload-OPTIONAL** (event-log-store.md §2.1.1, J4); the dual-write path
221
- in `src/core/event-log.ts:152` forwards `assignment_*` and `run_*` events to
222
- the journal with `item_id` only, no `payload`. The §2 rule "upsert when
223
- payload present, else a status/activity signal" means today's journal carries
224
- **no post-images for `assignment` or `agent_run`**: the in-memory projection
225
- has zero rows for those item_types, and the materializer
226
- (`src/core/events/materialize.ts`) only enumerates the 5 memory families
227
- (constraint/decision/trap/handoff/plan).
228
-
229
- Consequence for the §6 mapping table: until phase 1.5 ships, the rows that
230
- the table claims `assignment` / `agent_run` / `claim` populate (IN_PROGRESS
231
- worker rows, ATTENTION blocked/failed, Recently terminal under IN_PROGRESS)
232
- cannot be drawn from the journal alone. A conforming observer in dual mode
233
- MUST:
234
-
235
- - Seed those sections at activation from a **single observer-flagged
236
- `bclaw_context(kind: "board_summary")`** call (no timer, no poll) — that
237
- read is lock-free under the §8 observer contract (validated against
238
- `getDispatchStatus` and `loadAssignment`/`loadAgentRun`, which are pure
239
- projection reads — `mcp-read-handlers.ts:1916`, `json-store.ts:47`); and
240
- - Mark those sections "live-view degraded" in the tooltip until phase 1.5,
241
- so the operator can tell journal-driven sections (memory entities,
242
- attention badges) from MCP-seeded sections (workers, lifecycle).
243
-
244
- Memory-entity sections (plan / constraint / decision / trap / handoff /
245
- sequence / handoff-derived ACTIVITY) ARE journal-driven today via the
246
- per-entity diff in `persistState` (`src/core/state.ts:400`) — the protocol
247
- delivers its full value for them.
248
-
249
- ### 6.2 Section ID glossary
250
-
251
- The §6 table uses display names; the canonical IDs in
252
- `vscode-extension/src/board-tree.ts:321` are `attention | in-progress |
253
- sprints | backlog | system | agents | candidates | activity | plans | claims
254
- | assignments | runs | actions | handoffs | sprint | traps | cross-project`.
255
- "Recently terminal" is **not** a top-level section — it is a sub-node
256
- rendered under `in-progress`. The board also surfaces `cross-project`
257
- (federation incoming signals) and `linked_projects`, neither of which has a
258
- single journal `item_type` today: cross-project signals arrive via the
259
- handoff/candidate streams (already covered), `linked_projects` is derived
260
- from project config and is intentionally NOT a journal concern.
261
-
262
- The projection is **state**, not administrative belief: a worker row's health
263
- comes from evidence in the records (commits/fs signals carried on
264
- registry-lifecycle payloads when present), not from a bare status field that the
265
- 2026-06-10 log proved lies. Where richer evidence requires it, the surface MAY
266
- call `bclaw_dispatch_status` through the actions client (§7) — that is a
267
- read-only MCP call, used sparingly (per visible terminal row), not a poll.
268
-
269
- ## 7. Actions go through a separate, lazy MCP client
270
-
271
- Mutations (accept candidate, release claim, dispatch, transition, complete step)
272
- are the *only* reason an observer talks to the MCP server. Rules:
273
-
274
- - One lazily-created MCP client per project, spun up on first action, idle-timed
275
- out after inactivity. Never created just to display.
276
- - Distinct from any agent session: the client identifies as an **observer
277
- principal** (see §8), so its calls never adopt an agent's claim/cursor.
278
- - After an action, the observer does NOT optimistically mutate its projection;
279
- it waits for the resulting journal record(s) to arrive via the tail (§5) and
280
- re-projects. Single source of truth, no split-brain. (A short-lived "pending"
281
- affordance on the clicked item is a UI concern, not projection state.)
282
- - `bclaw_dispatch_status` and other read-only facades are permitted through this
283
- client for on-demand evidence enrichment, but are never on a timer.
284
-
285
- ## 8. Observer identity (no impersonation, no side effects)
286
-
287
- The surface declares itself an observer so the server suppresses every write a
288
- read would otherwise trigger:
289
-
290
- - Transport signal: `BRAINCLAW_OBSERVER=1` in the action client's env, and/or
291
- MCP `clientInfo.name = "brainclaw-observer/<surface>"`.
292
- - Server contract (already implemented, pln#558): observer reads do not
293
- `autoAcknowledge`, do not run agent-run reconciliation, do not advance
294
- `readUnseenEvents` cursors, do not implicit-heartbeat or auto-register an
295
- identity. This protocol is the client half of that contract: even the read-
296
- only facade calls in §7 carry the observer flag.
297
- - The observer never presents an agent name as the actor of anything. Actions
298
- the human triggers are attributed to the human operator principal, not to a
299
- spawned agent.
300
-
301
- ## 9. Failure modes and degradation
302
-
303
- | Condition | Behavior |
304
- |---|---|
305
- | `events/` absent (journal off / not migrated) | Fall back to a single MCP `board_summary` read at activation (no timer); show a "journal off — limited live view" hint. The surface still works, just not push-driven. |
306
- | Cursor gap (archived past watermark) | Cold-start from newest checkpoint (§5); silent — state is correct, only missed-activity history is lost. |
307
- | Checkpoint missing/corrupt | Fall back to the previous checkpoint, replay more segments (the two-checkpoint floor guarantees one exists); if none, seed empty + full tail. |
308
- | Torn / unparseable line | Skip, keep tailing (§5). |
309
- | Active segment shrinks / meta regresses | Trust the journal tail, re-derive; never write a "repair" (that is an agent/doctor job). |
310
- | Watch unavailable (network FS, sandbox) | Degrade to a long-interval stat of the active segment; still zero locks. |
311
-
312
- ## 10. Performance budget (vision §5.3, restated as observer obligations)
313
-
314
- | Operation | Target | Hard limit | How the protocol meets it |
315
- |---|---|---|---|
316
- | Activation → first summary | 500 ms | 2 s | seed from newest checkpoint, no full replay |
317
- | Summary refresh | 300 ms | 1 s | apply only the new tail records |
318
- | Section expand (warm) | 50 ms | 200 ms | projection is in memory; expand reads the map |
319
- | Section expand (cold) | 500 ms | 2 s | first projection build from checkpoint+tail |
320
- | Action round-trip | 500 ms | 2 s | lazy MCP client; result observed via tail |
321
-
322
- Out of budget → surface in tooltip + a "performance degraded" status-bar
323
- indicator (never escalate by calling a heavier path — that is the contention-
324
- breeds-contention bug this protocol exists to kill).
325
-
326
- ## 11. Language-agnostic conformance checklist
327
-
328
- A surface in any language conforms iff:
329
-
330
- 1. It reads only files under `.brainclaw/events/` (+ checkpoints) and writes
331
- nothing under `.brainclaw/`.
332
- 2. Its cursor is a seq watermark in client-private storage, keyed by
333
- `project_id`, never in the store's `.cursors/`.
334
- 3. It seeds from the newest verified checkpoint and tails by (segment, line)
335
- order, applying records by action *class*, tolerant of unknown verbs and torn
336
- tails.
337
- 4. Change detection is an OS file watch (or long-interval stat) — never an MCP
338
- poll, never a lock.
339
- 5. Mutations go through a separate lazy MCP client flagged as an observer
340
- principal; the projection updates only from the resulting journal records.
341
- 6. `attention_required` and worker health are computed from journal evidence,
342
- not from administrative status fields alone.
343
-
344
- Reference implementation: the VS Code extension (pln#560 step 2). The JetBrains
345
- plugin (next plan) implements this same checklist in Kotlin — its existence is
346
- the cross-language validation that this protocol, not the TypeScript code, is
347
- the contract.
348
-
349
- ## 12. OPEN QUESTIONS
350
-
351
- Carried from the 2026-06-12 symmetric review (pln#560 step 1, this branch).
352
- Each is something the spec text cannot close on its own; one or more must
353
- be answered before the JetBrains plugin (Kotlin) ships.
354
-
355
- | # | Sev | Question |
356
- |---|---|---|
357
- | O1 | MED | **Shared `ACTION_CLASS_BY_ACTION` manifest.** §2 says implementations MUST mirror the table "or fetch it from a shared manifest." Today only the TS version exists (`src/core/events/journal.ts:66`); a Kotlin implementer would re-type 42 entries by hand and silently drift on the 43rd. Should this ship as a generated JSON next to `event-log-store.md` (single source of truth, both runtimes load it) or as part of a versioned schema bundle? Recommend the generated JSON — the table is small and changes per spec revision, not per release. |
358
- | O2 | RESOLVED (pln#568) | **Phase-1.5 cutover signal for §6.1.** Resolved with a `journal_note` kind **`registry_genesis`** marker emitted by `runRegistryGenesisSupplement` after it backfills every pre-existing registry entity (`brainclaw migrate --enable-journal`). Observers detect it (`BoardObserver.registryAuthoritative()`, sticky + re-derived on cold start) and switch the registry counts from the MCP seed to the journal via `mergeCounts(..., registryAuthoritative)`. Chosen over a `meta.json` version bump because meta is a rebuildable cache (§2.3) — the marker is a durable journal record, the source of truth, and survives a meta rebuild. Open follow-up: when checkpoints start emitting (today `checkpoint_seq=0`), the checkpoint must encode the capability so a cold start past the marker's segment still re-derives authority. |
359
- | O3 | LOW | **`bclaw_dispatch_status` enrichment scope.** §6 + §7 allow it "per visible terminal row" but the wording is ambiguous between "terminal-state row" (Recently terminal) and "row currently visible in the terminal UI" (every IN_PROGRESS row). Settle: probably the first (only failed/silent_death rows want the evidence digest) — but the contract must say so, otherwise an implementor renders an O(workers) burst on every refresh. |
360
- | O4 | LOW | **Segment pad-width upgrade path.** §2 pins 8-digit decimal padding; the writer is 8-digit too (`src/core/events/journal.ts:214 SEGMENT_PAD=8`). At ~17k events historical, the 1e8 ceiling is decades out — but a future widen would require coordinated writer + every observer roll-out. Carry the migration recipe (pad-width in `meta.json`?) here so a future maintainer doesn't have to rediscover it. |
361
- | O5 | LOW | **File watch semantics on Windows network mounts.** §4 falls back to a "long-interval stat" when the watcher is unavailable. VS Code's `FileSystemWatcher` on a junction-linked worktree (the brainclaw dispatch substrate) may fire on the link target's mtime but not the source-of-truth segment writes from another process; verify against the dispatch worktree machinery (`pln#498` junctions) before declaring the watch path universal. |
1
+ # Observer Protocol — language-agnostic read-only surfaces
2
+
3
+ Status: spec (pln#560 step 1). Pivot deliverable: it serves the VS Code
4
+ extension, the JetBrains plugin, and any future surface identically. Companion
5
+ to `event-log-store.md` (the journal this protocol consumes) and the VS Code
6
+ vision §5 (the UX it powers).
7
+
8
+ ## 1. The one rule
9
+
10
+ **An observability surface is a pure consumer of the event journal. It never
11
+ acquires a store lock, never writes inside `.brainclaw/`, never runs a polling
12
+ timer against the MCP server for display.** It tails the append-only journal,
13
+ projects board state in memory, and refreshes the affected section when a new
14
+ record arrives. The MCP server is reserved for *actions* (accept / release /
15
+ dispatch / transition) through a separate, lazily-created client.
16
+
17
+ Why this exists (2026-06-10 calibration): the prior extension "read" the board
18
+ by calling a path that mutated and git-committed the entire store under the
19
+ lock (`autoAcknowledge → persistState`, held >5s), ran the agent-run reconciler
20
+ twice per poll with locked writes, created ~120 locked "unverified" event files
21
+ per hour per run, and impersonated the parent shell's agent identity —
22
+ consuming that agent's cursor. A dashboard is not an agent. This protocol makes
23
+ that class of bug unrepresentable: a conforming observer *cannot* write.
24
+
25
+ ## 2. What the observer reads
26
+
27
+ The journal defined in `event-log-store.md`:
28
+
29
+ ```
30
+ .brainclaw/events/
31
+ meta.json # { next_seq, active_segment, entity_revs } — a cache
32
+ seg-<firstSeq>.jsonl # immutable once rolled; named by first seq it holds
33
+ seg-<firstSeq>.jsonl # active = lexicographically-last segment
34
+ checkpoints/ # NOT YET EMITTED by the writer (see §5) — directory
35
+ ckpt-<seq>.json # may be absent today; self-contained state manifest
36
+ ```
37
+
38
+ **Segment naming is normative.** `firstSeq` is encoded as a **decimal,
39
+ zero-padded to 8 digits** (e.g. `seg-00018342.jsonl`) so directory-listing
40
+ lex-sort matches numeric seq order with no parsing. Implementations MUST emit
41
+ and accept exactly this format; any other padding (or none) breaks the
42
+ binary-search-by-filename in §5. The 8-digit field overflows at `seq > 1e8`;
43
+ at the historical write rate (~17k events to date) this is decades away, but
44
+ a journal that crosses the boundary requires a coordinated widen-and-pad
45
+ migration in writer + every observer (deferred until needed; flagged here so
46
+ no implementer assumes the pad width is incidental).
47
+
48
+ `meta.json` is advisory only (§4): the observer reads it to cheaply detect
49
+ "did anything change" but never trusts its `entity_revs` or `next_seq` for
50
+ correctness — those are derived from the journal tail itself.
51
+
52
+ Record envelope (v2), one JSON object per line:
53
+
54
+ ```jsonc
55
+ { "v": 2, "seq": 18342, "ts": "…", "writer": "w_…", "agent": "claude-code",
56
+ "action": "update", "item_type": "plan", "item_id": "pln_…",
57
+ "entity_rev": 7, "summary": "…", "payload": { /* full post-image */ } }
58
+ ```
59
+
60
+ Action → class mapping (the observer needs the class, not a hardcoded verb
61
+ list — it is `ACTION_CLASS_BY_ACTION` in `event-log.ts`, and MUST be mirrored
62
+ in every implementation or fetched from a shared manifest):
63
+
64
+ | Class | Effect on the projection |
65
+ |---|---|
66
+ | `entity-state` (`create`,`update`,`accept`,`reject`,`claim`,`release_claim`,`rollback`,`upgrade`,`backfill`) | upsert `payload` at `(item_type,item_id)` |
67
+ | `tombstone` (`delete`) | remove `(item_type,item_id)` |
68
+ | `journal-meta` (`checkpoint_ref`,`journal_note`,`seq_repair`,`federation_apply`) | ignore for state; `checkpoint_ref` is a bootstrap hint (§5) |
69
+ | `observability` (`session_start`,`session_end`,`assignment_offered`,`assignment_progress`,`run_progress`) | activity feed only — never a state upsert |
70
+ | `registry-lifecycle` (`assignment_*`,`run_*`) | upsert when `payload` present (phase 1.5+), else a status/activity signal |
71
+
72
+ The observer is **forward-compatible**: an unknown `action` whose class it
73
+ cannot resolve is applied as `entity-state` iff it carries a `payload` and an
74
+ `item_id`, else treated as an activity signal. Never crash on an unknown verb.
75
+
76
+ ## 3. The cursor lives OUTSIDE the store
77
+
78
+ The observer's read position is a **seq watermark** persisted in *client*
79
+ storage, never in `.brainclaw/`:
80
+
81
+ - VS Code: `ExtensionContext.workspaceState`, key `bclaw.observer.cursor.<project_id>`.
82
+ - JetBrains: `PropertiesComponent` / project-scoped state, same key shape.
83
+ - Generic: any client-private kv keyed by `project_id`.
84
+
85
+ Shape: `{ seq: number, checkpoint_seq: number }`. `seq` = highest record seq
86
+ applied; `checkpoint_seq` = the checkpoint the in-memory projection was last
87
+ seeded from (for fast re-bootstrap). The store's own `.cursors/` directory is
88
+ the AGENTS' read position and is **off-limits** to observers — touching it is
89
+ the identity-leak bug this protocol forbids.
90
+
91
+ Rationale: a watermark survives segment rotation, compaction, and archival
92
+ (byte offsets do not). It is private to the surface, so N observers never
93
+ interfere with each other or with agents.
94
+
95
+ ## 4. Change detection — a file watch, not a poll, not a lock
96
+
97
+ The observer watches the journal directory for growth and reacts:
98
+
99
+ 1. Watch `.brainclaw/events/` (the active segment's size/mtime, and creation of
100
+ new `seg-*.jsonl`). VS Code: `FileSystemWatcher` on `events/seg-*.jsonl` +
101
+ `meta.json`. JetBrains: `VirtualFileListener` / NIO `WatchService`. Generic
102
+ fallback: stat the active segment on a *long* interval (≥10 s) — this is a
103
+ stat, not an MCP call, and acquires no lock.
104
+ 2. On a growth signal, **tail forward** from `cursor.seq` (§5) and apply records
105
+ to the in-memory projection.
106
+ 3. `meta.json` is advisory only; never trust it for correctness — the journal
107
+ tail is the truth (it may be a stale cache mid-write). Use it only to detect
108
+ "did anything change" cheaply.
109
+
110
+ There is no MCP server process for display. The watcher is OS-level; the read
111
+ is a file read. Under the 2026-06-10 load (3 workers + open surface) this yields
112
+ zero lock acquisitions by the surface — the validation gate (step 3).
113
+
114
+ ## 5. Bootstrap and tail algorithm
115
+
116
+ **Status of checkpoint emission (2026-06-12, pln#543 step 4 landed):** the
117
+ writer does NOT yet produce `checkpoints/ckpt-*.json` files — checkpoint
118
+ emission ships with step 3/5 of pln#543. Until then, the empty-seed + full
119
+ tail path below is the **primary** cold-start path in production, not a
120
+ degenerate fallback. The checkpoint-first path is the spec the consumer must
121
+ implement for forward-compatibility; an observer that hard-requires a
122
+ checkpoint at activation is broken against today's store. The perf targets
123
+ in §10 assume checkpoint emission; until it ships, "activation → first
124
+ summary" is bounded by the full tail length instead (~10 MB of segments in
125
+ the typical case, sub-second in practice, but the budget no longer has
126
+ slack).
127
+
128
+ **Cold start (no cursor, or cursor below the oldest live segment):**
129
+
130
+ 1. **If a verified checkpoint exists**, load the newest `ckpt-<S>.json` →
131
+ seed the in-memory projection (full post-image set at head `S`). Set
132
+ `cursor.checkpoint_seq = S`. (Today: this branch is dead until the
133
+ writer ships checkpoint emission.)
134
+ 2. **Otherwise** (today's primary path), seed from the empty projection
135
+ with `cursor.checkpoint_seq = 0`.
136
+ 3. Tail every record with `seq > checkpoint_seq` across segments in
137
+ (segment, file-line) order; apply by class (§2). With no checkpoint
138
+ this is a full replay from seq 1, bounded by retention (the
139
+ `events/archive/` floor — segments are park-don't-deleted past the
140
+ second-newest verified checkpoint; with no checkpoint, no segment is
141
+ ever eligible for archive, so "the journal" = "every segment ever
142
+ written" until checkpoint emission ships).
143
+ 4. Set `cursor.seq` to the last applied seq. Render.
144
+
145
+ If the cursor's `seq` is **below the oldest non-archived segment's first
146
+ seq** (gap — segments archived past the watermark), discard the cursor and
147
+ cold-start: notifications degrade, state never does. Today no segment is
148
+ ever archived (gc requires a verified checkpoint floor, see §2.3 of the
149
+ store spec), so this branch is unreachable in production until checkpoint
150
+ emission ships — but the rule is normative regardless: an observer that
151
+ crashes on a gap is broken against any future store.
152
+
153
+ **Warm tail (cursor present, within live segments):**
154
+
155
+ 1. Binary-search the segment whose name (`seg-<firstSeq>`) contains
156
+ `cursor.seq + 1` (filenames sort by first seq).
157
+ 2. Stream forward from that point across segments; apply by class.
158
+ 3. A **torn tail** (final line unparseable or missing trailing `\n`) is expected
159
+ crash residue mid-write by an agent — skip it; it reappears complete on the
160
+ next growth signal. Never block on it.
161
+ 4. A mid-file unparseable line is logged and skipped (do not halt the tail).
162
+ 5. Advance `cursor.seq` only over records actually applied.
163
+
164
+ Replay order is always (segment order, then file-line order) — never sorted by
165
+ seq (matches the store's own reducer; a dup `seq` from a lock-steal applies
166
+ later-line-wins, harmlessly, in a read-only projection).
167
+
168
+ ## 6. Board projection — which records touch which section
169
+
170
+ The in-memory projection is `Map<item_type, Map<item_id, payload>>` plus a
171
+ bounded recent-activity ring (observability + registry signals, last N). The
172
+ board sections are derived; a record invalidates only the sections its
173
+ `item_type` feeds, and only those re-render (push-by-affected-section, §5.3):
174
+
175
+ | `item_type` | Invalidates sections |
176
+ |---|---|
177
+ | `plan` | IN_PROGRESS, SPRINTS, BACKLOG, ATTENTION (badge), SYSTEM (counts) |
178
+ | `claim` | IN_PROGRESS, AGENTS (roster freshness) |
179
+ | `assignment` | IN_PROGRESS, ATTENTION (blocked/failed), "Recently terminal" |
180
+ | `agent_run` | IN_PROGRESS (worker rows), AGENTS, "Recently terminal" |
181
+ | `candidate` | ATTENTION (human-review), CANDIDATES |
182
+ | `action` | ATTENTION (the dominant attention input) |
183
+ | `constraint`/`decision`/`trap` | SYSTEM (counts), TRAPS |
184
+ | `handoff` | ACTIVITY, SYSTEM (counts) |
185
+ | `sequence` | SPRINTS |
186
+ | `session`/`*_progress` (observability) | ACTIVITY feed only — never a section state change |
187
+
188
+ `attention_required` is computed by the observer from the projection (actions +
189
+ human candidates + blocked/failed assignments + failed runs + evidence-
190
+ contradicted terminals), matching what the server-side composite returns — the
191
+ surface must not under-count by reading "actions only" (the pln#559 fix, now in
192
+ the projection rule).
193
+
194
+ ### 6.1 Dual-mode coverage gap (CLOSED by pln#568 phase 1.5)
195
+
196
+ > **Status (pln#568):** the writer-side gap below is **closed**. The
197
+ > registry / coordination families (claim, assignment, agent_run,
198
+ > action_required [journaled under item_type `state`], candidate, sequence,
199
+ > and SHARED runtime_note) now emit full entity-state **post-images** on their
200
+ > persist chokepoint (`src/core/events/registry-post-image.ts`), and the
201
+ > observer materializer projects them (`board-projection.ts` ARRAY_SLOT).
202
+ >
203
+ > **Cutover signal (O2, resolved):** an observer switches a registry family
204
+ > from the MCP `board_summary` seed to the journal only once the journal
205
+ > carries the `journal_note` kind **`registry_genesis`** marker — emitted by
206
+ > `runRegistryGenesisSupplement` (run via `brainclaw migrate --enable-journal`)
207
+ > after it backfills every pre-existing registry entity. The marker is the
208
+ > safety gate: without a complete backfill a partially-journaled store would
209
+ > undercount the attention badge (trp#559). `BoardObserver.registryAuthoritative()`
210
+ > tracks the marker (sticky, re-derived on cold start by replaying from the
211
+ > checkpoint floor); `mergeCounts(journal, seed, journalActive, registryAuthoritative)`
212
+ > takes claims/assignments/runs/actions from the journal when it is set, and
213
+ > from the seed otherwise. `agents`/`sessions` are never journaled → always seed.
214
+ > A store that has NOT run the supplement keeps the seed (no regression).
215
+ >
216
+ > The historical (pre-pln#568) description below is kept for context.
217
+
218
+ The journal classifies records into five classes (§2). In phase 1 / `dual`
219
+ mode — what runs today after pln#543 step 4 — **registry-lifecycle records
220
+ are payload-OPTIONAL** (event-log-store.md §2.1.1, J4); the dual-write path
221
+ in `src/core/event-log.ts:152` forwards `assignment_*` and `run_*` events to
222
+ the journal with `item_id` only, no `payload`. The §2 rule "upsert when
223
+ payload present, else a status/activity signal" means today's journal carries
224
+ **no post-images for `assignment` or `agent_run`**: the in-memory projection
225
+ has zero rows for those item_types, and the materializer
226
+ (`src/core/events/materialize.ts`) only enumerates the 5 memory families
227
+ (constraint/decision/trap/handoff/plan).
228
+
229
+ Consequence for the §6 mapping table: until phase 1.5 ships, the rows that
230
+ the table claims `assignment` / `agent_run` / `claim` populate (IN_PROGRESS
231
+ worker rows, ATTENTION blocked/failed, Recently terminal under IN_PROGRESS)
232
+ cannot be drawn from the journal alone. A conforming observer in dual mode
233
+ MUST:
234
+
235
+ - Seed those sections at activation from a **single observer-flagged
236
+ `bclaw_context(kind: "board_summary")`** call (no timer, no poll) — that
237
+ read is lock-free under the §8 observer contract (validated against
238
+ `getDispatchStatus` and `loadAssignment`/`loadAgentRun`, which are pure
239
+ projection reads — `mcp-read-handlers.ts:1916`, `json-store.ts:47`); and
240
+ - Mark those sections "live-view degraded" in the tooltip until phase 1.5,
241
+ so the operator can tell journal-driven sections (memory entities,
242
+ attention badges) from MCP-seeded sections (workers, lifecycle).
243
+
244
+ Memory-entity sections (plan / constraint / decision / trap / handoff /
245
+ sequence / handoff-derived ACTIVITY) ARE journal-driven today via the
246
+ per-entity diff in `persistState` (`src/core/state.ts:400`) — the protocol
247
+ delivers its full value for them.
248
+
249
+ ### 6.2 Section ID glossary
250
+
251
+ The §6 table uses display names; the canonical IDs in
252
+ `vscode-extension/src/board-tree.ts:321` are `attention | in-progress |
253
+ sprints | backlog | system | agents | candidates | activity | plans | claims
254
+ | assignments | runs | actions | handoffs | sprint | traps | cross-project`.
255
+ "Recently terminal" is **not** a top-level section — it is a sub-node
256
+ rendered under `in-progress`. The board also surfaces `cross-project`
257
+ (federation incoming signals) and `linked_projects`, neither of which has a
258
+ single journal `item_type` today: cross-project signals arrive via the
259
+ handoff/candidate streams (already covered), `linked_projects` is derived
260
+ from project config and is intentionally NOT a journal concern.
261
+
262
+ The projection is **state**, not administrative belief: a worker row's health
263
+ comes from evidence in the records (commits/fs signals carried on
264
+ registry-lifecycle payloads when present), not from a bare status field that the
265
+ 2026-06-10 log proved lies. Where richer evidence requires it, the surface MAY
266
+ call `bclaw_dispatch_status` through the actions client (§7) — that is a
267
+ read-only MCP call, used sparingly (per visible terminal row), not a poll.
268
+
269
+ ## 7. Actions go through a separate, lazy MCP client
270
+
271
+ Mutations (accept candidate, release claim, dispatch, transition, complete step)
272
+ are the *only* reason an observer talks to the MCP server. Rules:
273
+
274
+ - One lazily-created MCP client per project, spun up on first action, idle-timed
275
+ out after inactivity. Never created just to display.
276
+ - Distinct from any agent session: the client identifies as an **observer
277
+ principal** (see §8), so its calls never adopt an agent's claim/cursor.
278
+ - After an action, the observer does NOT optimistically mutate its projection;
279
+ it waits for the resulting journal record(s) to arrive via the tail (§5) and
280
+ re-projects. Single source of truth, no split-brain. (A short-lived "pending"
281
+ affordance on the clicked item is a UI concern, not projection state.)
282
+ - `bclaw_dispatch_status` and other read-only facades are permitted through this
283
+ client for on-demand evidence enrichment, but are never on a timer.
284
+
285
+ ## 8. Observer identity (no impersonation, no side effects)
286
+
287
+ The surface declares itself an observer so the server suppresses every write a
288
+ read would otherwise trigger:
289
+
290
+ - Transport signal: `BRAINCLAW_OBSERVER=1` in the action client's env, and/or
291
+ MCP `clientInfo.name = "brainclaw-observer/<surface>"`.
292
+ - Server contract (already implemented, pln#558): observer reads do not
293
+ `autoAcknowledge`, do not run agent-run reconciliation, do not advance
294
+ `readUnseenEvents` cursors, do not implicit-heartbeat or auto-register an
295
+ identity. This protocol is the client half of that contract: even the read-
296
+ only facade calls in §7 carry the observer flag.
297
+ - The observer never presents an agent name as the actor of anything. Actions
298
+ the human triggers are attributed to the human operator principal, not to a
299
+ spawned agent.
300
+
301
+ ## 9. Failure modes and degradation
302
+
303
+ | Condition | Behavior |
304
+ |---|---|
305
+ | `events/` absent (journal off / not migrated) | Fall back to a single MCP `board_summary` read at activation (no timer); show a "journal off — limited live view" hint. The surface still works, just not push-driven. |
306
+ | Cursor gap (archived past watermark) | Cold-start from newest checkpoint (§5); silent — state is correct, only missed-activity history is lost. |
307
+ | Checkpoint missing/corrupt | Fall back to the previous checkpoint, replay more segments (the two-checkpoint floor guarantees one exists); if none, seed empty + full tail. |
308
+ | Torn / unparseable line | Skip, keep tailing (§5). |
309
+ | Active segment shrinks / meta regresses | Trust the journal tail, re-derive; never write a "repair" (that is an agent/doctor job). |
310
+ | Watch unavailable (network FS, sandbox) | Degrade to a long-interval stat of the active segment; still zero locks. |
311
+
312
+ ## 10. Performance budget (vision §5.3, restated as observer obligations)
313
+
314
+ | Operation | Target | Hard limit | How the protocol meets it |
315
+ |---|---|---|---|
316
+ | Activation → first summary | 500 ms | 2 s | seed from newest checkpoint, no full replay |
317
+ | Summary refresh | 300 ms | 1 s | apply only the new tail records |
318
+ | Section expand (warm) | 50 ms | 200 ms | projection is in memory; expand reads the map |
319
+ | Section expand (cold) | 500 ms | 2 s | first projection build from checkpoint+tail |
320
+ | Action round-trip | 500 ms | 2 s | lazy MCP client; result observed via tail |
321
+
322
+ Out of budget → surface in tooltip + a "performance degraded" status-bar
323
+ indicator (never escalate by calling a heavier path — that is the contention-
324
+ breeds-contention bug this protocol exists to kill).
325
+
326
+ ## 11. Language-agnostic conformance checklist
327
+
328
+ A surface in any language conforms iff:
329
+
330
+ 1. It reads only files under `.brainclaw/events/` (+ checkpoints) and writes
331
+ nothing under `.brainclaw/`.
332
+ 2. Its cursor is a seq watermark in client-private storage, keyed by
333
+ `project_id`, never in the store's `.cursors/`.
334
+ 3. It seeds from the newest verified checkpoint and tails by (segment, line)
335
+ order, applying records by action *class*, tolerant of unknown verbs and torn
336
+ tails.
337
+ 4. Change detection is an OS file watch (or long-interval stat) — never an MCP
338
+ poll, never a lock.
339
+ 5. Mutations go through a separate lazy MCP client flagged as an observer
340
+ principal; the projection updates only from the resulting journal records.
341
+ 6. `attention_required` and worker health are computed from journal evidence,
342
+ not from administrative status fields alone.
343
+
344
+ Reference implementation: the VS Code extension (pln#560 step 2). The JetBrains
345
+ plugin (next plan) implements this same checklist in Kotlin — its existence is
346
+ the cross-language validation that this protocol, not the TypeScript code, is
347
+ the contract.
348
+
349
+ ## 12. OPEN QUESTIONS
350
+
351
+ Carried from the 2026-06-12 symmetric review (pln#560 step 1, this branch).
352
+ Each is something the spec text cannot close on its own; one or more must
353
+ be answered before the JetBrains plugin (Kotlin) ships.
354
+
355
+ | # | Sev | Question |
356
+ |---|---|---|
357
+ | O1 | MED | **Shared `ACTION_CLASS_BY_ACTION` manifest.** §2 says implementations MUST mirror the table "or fetch it from a shared manifest." Today only the TS version exists (`src/core/events/journal.ts:66`); a Kotlin implementer would re-type 42 entries by hand and silently drift on the 43rd. Should this ship as a generated JSON next to `event-log-store.md` (single source of truth, both runtimes load it) or as part of a versioned schema bundle? Recommend the generated JSON — the table is small and changes per spec revision, not per release. |
358
+ | O2 | RESOLVED (pln#568) | **Phase-1.5 cutover signal for §6.1.** Resolved with a `journal_note` kind **`registry_genesis`** marker emitted by `runRegistryGenesisSupplement` after it backfills every pre-existing registry entity (`brainclaw migrate --enable-journal`). Observers detect it (`BoardObserver.registryAuthoritative()`, sticky + re-derived on cold start) and switch the registry counts from the MCP seed to the journal via `mergeCounts(..., registryAuthoritative)`. Chosen over a `meta.json` version bump because meta is a rebuildable cache (§2.3) — the marker is a durable journal record, the source of truth, and survives a meta rebuild. Open follow-up: when checkpoints start emitting (today `checkpoint_seq=0`), the checkpoint must encode the capability so a cold start past the marker's segment still re-derives authority. |
359
+ | O3 | LOW | **`bclaw_dispatch_status` enrichment scope.** §6 + §7 allow it "per visible terminal row" but the wording is ambiguous between "terminal-state row" (Recently terminal) and "row currently visible in the terminal UI" (every IN_PROGRESS row). Settle: probably the first (only failed/silent_death rows want the evidence digest) — but the contract must say so, otherwise an implementor renders an O(workers) burst on every refresh. |
360
+ | O4 | LOW | **Segment pad-width upgrade path.** §2 pins 8-digit decimal padding; the writer is 8-digit too (`src/core/events/journal.ts:214 SEGMENT_PAD=8`). At ~17k events historical, the 1e8 ceiling is decades out — but a future widen would require coordinated writer + every observer roll-out. Carry the migration recipe (pad-width in `meta.json`?) here so a future maintainer doesn't have to rediscover it. |
361
+ | O5 | LOW | **File watch semantics on Windows network mounts.** §4 falls back to a "long-interval stat" when the watcher is unavailable. VS Code's `FileSystemWatcher` on a junction-linked worktree (the brainclaw dispatch substrate) may fire on the link target's mtime but not the source-of-truth segment writes from another process; verify against the dispatch worktree machinery (`pln#498` junctions) before declaring the watch path universal. |