brainclaw 1.7.5 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +28 -11
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +139 -13
  4. package/dist/commands/add-step.js +1 -1
  5. package/dist/commands/bootstrap.js +2 -26
  6. package/dist/commands/check-security-mcp.js +50 -33
  7. package/dist/commands/check-security.js +86 -43
  8. package/dist/commands/claim.js +22 -21
  9. package/dist/commands/confirm.js +26 -0
  10. package/dist/commands/context-diff.js +1 -1
  11. package/dist/commands/dispatch-watch.js +142 -0
  12. package/dist/commands/doctor.js +113 -2
  13. package/dist/commands/estimation-report.js +115 -16
  14. package/dist/commands/harvest.js +502 -16
  15. package/dist/commands/init.js +123 -21
  16. package/dist/commands/loops-handlers.js +4 -0
  17. package/dist/commands/mcp-read-handlers.js +198 -29
  18. package/dist/commands/mcp.js +615 -92
  19. package/dist/commands/memory.js +21 -17
  20. package/dist/commands/migrate.js +81 -17
  21. package/dist/commands/prune.js +78 -4
  22. package/dist/commands/reflect.js +26 -20
  23. package/dist/commands/register-agent.js +57 -1
  24. package/dist/commands/repair.js +20 -0
  25. package/dist/commands/session-end.js +15 -6
  26. package/dist/commands/session-start.js +18 -1
  27. package/dist/commands/setup-security.js +39 -18
  28. package/dist/commands/setup.js +26 -27
  29. package/dist/commands/stale.js +16 -2
  30. package/dist/commands/uninstall.js +126 -34
  31. package/dist/commands/update-step.js +6 -0
  32. package/dist/commands/worktree.js +60 -0
  33. package/dist/core/actions.js +12 -3
  34. package/dist/core/agent-capability.js +11 -13
  35. package/dist/core/agent-files.js +844 -547
  36. package/dist/core/agent-integrations.js +0 -3
  37. package/dist/core/agent-inventory.js +67 -0
  38. package/dist/core/agent-registry.js +163 -29
  39. package/dist/core/agentrun-reconciler.js +33 -2
  40. package/dist/core/agentruns.js +7 -1
  41. package/dist/core/ai-agent-detection.js +31 -44
  42. package/dist/core/archival.js +15 -9
  43. package/dist/core/assignment-reconciler.js +56 -0
  44. package/dist/core/assignment-sweeper.js +127 -4
  45. package/dist/core/assignments.js +69 -11
  46. package/dist/core/bootstrap.js +233 -67
  47. package/dist/core/brainclaw-version.js +22 -0
  48. package/dist/core/candidates.js +21 -1
  49. package/dist/core/claims.js +313 -150
  50. package/dist/core/config.js +6 -1
  51. package/dist/core/context-diff.js +148 -20
  52. package/dist/core/context.js +129 -8
  53. package/dist/core/coordination.js +22 -3
  54. package/dist/core/dispatch-status.js +109 -5
  55. package/dist/core/dispatcher.js +65 -11
  56. package/dist/core/entity-operations.js +45 -24
  57. package/dist/core/entity-registry.js +31 -5
  58. package/dist/core/event-log.js +138 -21
  59. package/dist/core/events/checkpoint.js +258 -0
  60. package/dist/core/events/genesis.js +220 -0
  61. package/dist/core/events/journal.js +507 -0
  62. package/dist/core/events/materialize.js +126 -0
  63. package/dist/core/events/registry-post-image.js +110 -0
  64. package/dist/core/events/verify.js +109 -0
  65. package/dist/core/execution-adapters.js +23 -0
  66. package/dist/core/execution.js +25 -0
  67. package/dist/core/facade-schema.js +48 -0
  68. package/dist/core/gc-semantic.js +130 -5
  69. package/dist/core/handoff-snapshot.js +68 -0
  70. package/dist/core/ids.js +19 -8
  71. package/dist/core/instruction-templates.js +34 -115
  72. package/dist/core/io.js +39 -3
  73. package/dist/core/json-store.js +10 -1
  74. package/dist/core/lock.js +153 -28
  75. package/dist/core/loops/bootstrap-acquire.js +25 -1
  76. package/dist/core/loops/facade-schema.js +2 -0
  77. package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
  78. package/dist/core/loops/index.js +1 -0
  79. package/dist/core/loops/presets/bootstrap.js +7 -0
  80. package/dist/core/loops/store.js +17 -0
  81. package/dist/core/loops/verbs.js +24 -1
  82. package/dist/core/markdown.js +8 -76
  83. package/dist/core/mcp-command-resolution.js +245 -0
  84. package/dist/core/memory-compactor.js +5 -3
  85. package/dist/core/memory-lifecycle.js +282 -0
  86. package/dist/core/merge-risk.js +150 -0
  87. package/dist/core/messaging.js +8 -1
  88. package/dist/core/migration.js +11 -1
  89. package/dist/core/observer-mode.js +26 -0
  90. package/dist/core/operations/memory-mutation.js +90 -65
  91. package/dist/core/operations/plan.js +27 -1
  92. package/dist/core/protocol-skills.js +210 -0
  93. package/dist/core/reflection-safety.js +6 -7
  94. package/dist/core/reputation.js +84 -2
  95. package/dist/core/runtime-signals.js +71 -9
  96. package/dist/core/runtime.js +84 -1
  97. package/dist/core/schema.js +125 -0
  98. package/dist/core/security-detectors.js +125 -0
  99. package/dist/core/security-extract.js +189 -0
  100. package/dist/core/security-guard.js +107 -29
  101. package/dist/core/security-packages.js +121 -0
  102. package/dist/core/security-scoring.js +76 -9
  103. package/dist/core/security.js +34 -2
  104. package/dist/core/sequence.js +11 -2
  105. package/dist/core/setup-flow.js +141 -13
  106. package/dist/core/spawn-check.js +110 -4
  107. package/dist/core/staleness.js +109 -1
  108. package/dist/core/state.js +250 -54
  109. package/dist/core/store-resolution.js +19 -5
  110. package/dist/core/worktree.js +169 -7
  111. package/dist/facts.js +8 -8
  112. package/dist/facts.json +7 -7
  113. package/docs/PROTOCOL.md +223 -0
  114. package/docs/cli.md +11 -10
  115. package/docs/concepts/coordinator-runbook.md +129 -0
  116. package/docs/concepts/dispatch-lifecycle.md +17 -0
  117. package/docs/concepts/event-log-store-critique-A.md +333 -0
  118. package/docs/concepts/event-log-store-critique-B.md +353 -0
  119. package/docs/concepts/event-log-store-phase0-measurements.md +58 -0
  120. package/docs/concepts/event-log-store-proposal-A.md +365 -0
  121. package/docs/concepts/event-log-store-proposal-B.md +404 -0
  122. package/docs/concepts/event-log-store.md +928 -0
  123. package/docs/concepts/identity-model-proposal.md +371 -0
  124. package/docs/concepts/memory.md +5 -4
  125. package/docs/concepts/observer-protocol.md +361 -0
  126. package/docs/concepts/parallel-merge-protocol.md +71 -0
  127. package/docs/concepts/plans-and-claims.md +43 -0
  128. package/docs/concepts/skills.md +78 -0
  129. package/docs/concepts/workspace-bootstrapping.md +61 -0
  130. package/docs/integrations/agents.md +4 -4
  131. package/docs/integrations/cline.md +10 -11
  132. package/docs/integrations/codex.md +2 -2
  133. package/docs/integrations/continue.md +5 -5
  134. package/docs/integrations/copilot.md +14 -12
  135. package/docs/integrations/openclaw.md +7 -6
  136. package/docs/integrations/overview.md +7 -7
  137. package/docs/integrations/roo.md +3 -3
  138. package/docs/integrations/windsurf.md +6 -6
  139. package/docs/mcp-schema-changelog.md +51 -20
  140. package/docs/quickstart.md +48 -47
  141. package/docs/security.md +174 -15
  142. package/docs/storage.md +4 -2
  143. package/package.json +8 -6
@@ -7,10 +7,11 @@
7
7
  * LIVE (gitignored, frequent refresh) — plans, claims, traps, decisions, sequences
8
8
  *
9
9
  * Tier delivery:
10
- * Tier A (MCP + hooks): stable file only — live context via hooks/MCP
10
+ * Tier A (managed MCP/native surface): stable file only — live context via MCP or native runtime surfaces
11
11
  * Tier B (MCP, no hooks): stable file + live companion file
12
12
  * Tier C (no MCP): stable file + live companion file (richer, only source)
13
13
  */
14
+ import { resolveExportTarget } from './agent-files.js';
14
15
  /**
15
16
  * Render the STABLE brainclaw section content for an instruction file.
16
17
  * This is the versioned file that changes rarely (on upgrade, convention change).
@@ -212,7 +213,7 @@ function renderVisionSection(input) {
212
213
  }
213
214
  function renderHeader(input) {
214
215
  return [
215
- `> Managed by brainclaw v${input.brainclawVersion} — do not edit manually.`,
216
+ `> Managed by brainclaw — do not edit manually.`,
216
217
  `> Regenerate: brainclaw export --format ${formatForAgent(input.profile.name)} --write`,
217
218
  ].join('\n');
218
219
  }
@@ -222,50 +223,30 @@ function renderLiveHeader(_input) {
222
223
  `> Last updated: ${new Date().toISOString().slice(0, 19)}`,
223
224
  ].join('\n');
224
225
  }
226
+ // Kept deliberately small (pln#542): entry point + grammar + escalation
227
+ // pointer. Everything else is discoverable via the `next_actions` carried by
228
+ // every MCP response — protocol teaching moved out of this file.
225
229
  function renderSessionProtocol() {
226
230
  return [
227
231
  '## brainclaw — session protocol',
228
232
  '',
229
- '1. Call `bclaw_work(intent)` to start working it handles session, context, and claims automatically. Returns a compact payload by default; pass `compact: false` for the full context result, or use `bclaw_context(kind="memory")` after.',
230
- '2. Use the canonical grammar (`bclaw_find` / `bclaw_get` / `bclaw_create` / `bclaw_update` / `bclaw_remove` / `bclaw_transition`) to work with memory objects (plans, decisions, constraints, traps, handoffs, claims, candidates, runtime_notes, …). Read `## brainclaw — working with memory` below for the full map.',
233
+ '1. Call `bclaw_work(intent)` (consult|execute|resume|review) one call handles session, context, and claim (execute). Every response carries `next_actions` with the exact follow-up calls: follow those instead of memorizing the API.',
234
+ '2. Canonical grammar for memory objects: `bclaw_find` / `bclaw_get` / `bclaw_create` / `bclaw_update` / `bclaw_remove` / `bclaw_transition` (entity, …).',
231
235
  '3. Do not assume project state without reading brainclaw context first.',
232
236
  '',
233
- '_Escalation path (only when you orchestrate other agents) — by goal:_',
234
- '- Start a code review / consult an agent / assign a scope → `bclaw_coordinate(intent=review|consult|assign)`',
235
- '- Parallelize execute across a sequence\'s lanes → `bclaw_dispatch(intent=execute)`',
236
- '- Drive a turn in a loop already assigned to you → `bclaw_loop(intent=turn|complete_turn|advance|close)`',
237
- '',
238
- 'Do NOT call `bclaw_loop(intent=open)` directly — it creates a loop structure without dispatch, so the reviewer/participant never gets the work. Use the goal entries above.',
239
- '',
240
- '_How to verify a dispatch actually worked:_ `execution_status="delivered_and_started"` only means the brief-ack sentinel was touched — it does NOT mean the worker is doing useful work. (1) Call `bclaw_dispatch_status(target_id=<asgn_…|clm_…|lop_…|run_…>)` — the purpose-built facade: it resolves the linked entities, reads the runtime sentinels (`ack` / `heartbeat` / `completed` / `failed`) and the captured stdout/stderr tails, checks pid liveness, and returns a single health verdict plus a recommended next action. This is the `verify_with` target named in the coordinate/dispatch response — prefer it over assembling the picture by hand. (2) Do NOT diagnose liveness from the tracked pid yourself: on Windows an ack-wrapped spawn runs under a `cmd.exe` shell, so `agent_run.pid` is the wrapper (which exits early by design), NOT the real worker — `Get-Process -Id <pid>` reads it dead while the worker is alive and committing. Trust the sentinel-derived verdict instead; the reconciler already infers `completed` from a post-start commit on the worktree branch even when the worker never called `bclaw_assignment_update`. (3) Fallback only if the facade is unavailable: `bclaw_find(entity="agent_run", filter={assignment_id})` plus the captured streams at `.brainclaw/coordination/runtime/log/<assignment_id>.{stdout,stderr}.log` — note that `claude -p` buffers stdout until exit, so an empty log mid-run is expected; use the `heartbeat` sentinel as the live progress signal, not stdout. Full FSM tables + diagnostic decision tree in `docs/concepts/dispatch-lifecycle.md`.',
237
+ 'Escalation (only when orchestrating other agents): `bclaw_coordinate(intent=review|consult|assign)`. Verify any dispatch with `bclaw_dispatch_status(target_id)` trust its sentinel-based verdict, not the tracked pid. Details: `docs/concepts/dispatch-lifecycle.md`.',
241
238
  ].join('\n');
242
239
  }
243
240
  function renderUserWorkflow() {
244
241
  return [
245
242
  '## brainclaw — user workflow',
246
243
  '',
247
- 'The intended end-to-end flow, executable by a single agent:',
248
- '',
249
244
  ' ideation → plan (+ steps) → claim → implement → release claim → review → close step/plan → merge',
250
245
  '',
251
- 'Multi-agent coordination is optional use the escalation path only when delegating to another agent.',
252
- '`sequence` is optional: add it between plan and claim when you want parallelized lanes across agents.',
253
- '',
254
- '**Entity → role in the flow:**',
255
- '- `plan` — intended outcome. Create with `bclaw_create(plan, …)`, decompose with `bclaw_add_step`.',
256
- '- `step` — incremental unit inside a plan; mark done with `bclaw_complete_step` as you implement.',
257
- '- `sequence` — ordered lanes when work can be parallelized across claims/agents (optional).',
258
- '- `claim` — advisory reservation of a scope before editing; release once implementation is ready for review.',
259
- '- `handoff` — immutable snapshot of what moved to the next stage (review, merge).',
260
- '- `candidate` — proposed decision / constraint / trap awaiting review before entering durable memory.',
261
- '- `decision` / `constraint` / `trap` / `runtime_note` — captured along the way to preserve context for future sessions.',
246
+ 'Entities: `plan` (intended outcome) · `step` (unit inside a plan) · `sequence` (optional parallel lanes) · `claim` (advisory scope reservation) · `handoff` (stage snapshot) · `candidate` (proposed memory awaiting review) · `decision`/`constraint`/`trap`/`runtime_note` (context captured along the way).',
262
247
  '',
263
- '**Review & Fix Loop (multi-turn delegation):**',
264
- '- Start: `bclaw_coordinate(intent=review, open_loop=true, review_mode=symmetric|asymmetric, targetAgents=[reviewer])`opens the loop AND dispatches the first turn to the reviewer.',
265
- '- Drive: `bclaw_loop(intent=turn|complete_turn|advance|close)` for turns assigned to your slot.',
266
- '- Anti-pattern: `bclaw_loop(intent=open)` alone — creates the loop structure without any dispatch, so nothing actually runs.',
267
- '',
268
- 'Ideation / Debug / Research / Planning loops — *planned*. See `docs/product/agent-first-model.md` §3.',
248
+ 'Review & Fix Loop: start with `bclaw_coordinate(intent=review, open_loop=true, review_mode=symmetric|asymmetric, targetAgents=[reviewer])` — opens the loop AND dispatches the first turn. Drive your turns with `bclaw_loop(intent=turn|complete_turn|advance|close)`. Parallelize a sequence\'s lanes with `bclaw_dispatch(intent=execute)`.',
249
+ 'Ideation / Debug / Research loops*planned* (`docs/product/agent-first-model.md` §3).',
269
250
  ].join('\n');
270
251
  }
271
252
  /**
@@ -288,51 +269,16 @@ function renderAutonomyContract() {
288
269
  return [
289
270
  '## brainclaw — autonomous workflow contract',
290
271
  '',
291
- 'When a brainclaw protocol prescribes the next action after the one you just',
292
- 'completed, **execute it. Do not ask for permission.** brainclaw\'s coordination',
293
- 'promise depends on this — every avoidable pause forces the human back into the',
294
- 'synchronization loop, which is exactly what brainclaw is built to eliminate.',
295
- '',
296
- '**Protocol-defined transitions you MUST execute autonomously:**',
297
- '',
298
- '- IF you completed a review (assigned via `bclaw_coordinate(intent=review)` or',
299
- ' inbox `review_request` message) → produce findings → **send the reply with',
300
- ' your verdict via `bclaw_send_message(type="reply", thread_id=…)`**. Do not',
301
- ' ask whether to send.',
302
- '- IF you receive a reply with an `LGTM` verdict AND you are the requester →',
303
- ' **close the loop and merge the branch yourself** (when you own the merge).',
304
- ' Do not pass the merge to the human.',
305
- '- IF the reviewer reply contains findings → **apply the symmetric fixes you',
306
- ' can make**, commit on the same branch, then reroute via',
307
- ' `bclaw_send_message(type="reply", …)` for re-review. Do not stop after the',
308
- ' fixes.',
309
- '- IF you have an active claim and your work is complete → call',
310
- ' `bclaw_release_claim(id=…, planStatus="done")`. Do not leave the claim',
311
- ' active for someone else to clean up.',
312
- '- IF you have an `assignment_id` (you were dispatched via `bclaw_coordinate` /',
313
- ' `bclaw_dispatch`) → at end of work, call',
314
- ' `bclaw_assignment_update(assignment_id=…, status="completed", artifacts=[…])`.',
315
- ' The coordinator depends on this lifecycle event; without it, the loop never',
316
- ' converges and the supervisor cannot tell success from silent failure.',
317
- '',
318
- '**Legitimate reasons to pause and ask the human:**',
272
+ 'When a brainclaw protocol prescribes the next action, **execute it. Do not ask for permission.** (Empirical: May 2026 multi-agent review threads stalled at "should I send this reply? / merge?", forcing the human to carry context between agents.)',
319
273
  '',
320
- '1. The action would be **destructive AND irreversible AND outside the',
321
- ' protocol** (e.g. `git push --force` on a public branch, dropping a',
322
- ' production database, deleting a non-empty memory cluster). Protocol-',
323
- ' defined merges to a local branch are NOT in this category.',
324
- '2. The protocol genuinely **does not specify** the next action (e.g. a',
325
- ' verdict that splits the dispatched work into two new plans — needs',
326
- ' scoping judgement only the human owns).',
327
- '3. The user has **explicitly told you to confirm** before specific actions',
328
- ' in this session.',
274
+ 'MUST execute autonomously:',
275
+ '- review done send the verdict: `bclaw_send_message(type="reply", thread_id=…)`',
276
+ '- LGTM received and you own the merge → close the loop and merge yourself',
277
+ '- findings received apply fixes, commit, reply for re-review',
278
+ '- work complete `bclaw_release_claim(id=…, planStatus="done")`',
279
+ '- dispatched work done `bclaw_assignment_update(assignment_id=…, status="completed", artifacts=[…])`',
329
280
  '',
330
- 'Asking *"should I send the reply?"* on a protocol-defined send is a UX bug,',
331
- 'not safety. If the next step appears in the workflow above — do it. The',
332
- 'empirical cost of skipping the contract: in May 2026, multi-agent review',
333
- 'threads systematically stalled at "should I send this reply?" / "should I',
334
- 'merge?", forcing the human supervisor to manually carry context between',
335
- 'agents. Stop reproducing that pattern.',
281
+ 'Pause for the human ONLY when the action is destructive AND irreversible AND outside the protocol, when the protocol does not specify the next step, or when the user explicitly asked for confirmation.',
336
282
  ].join('\n');
337
283
  }
338
284
  // ─── Constraint sections (split by category) ────────────────────────────────
@@ -371,33 +317,13 @@ function renderAvailableTools() {
371
317
  return [
372
318
  '## brainclaw — available tools',
373
319
  '',
374
- 'The default MCP catalog is intentionally small. Start with `bclaw_work`, then use the canonical grammar for reads/writes on any entity. Coordination facades below are an **escalation path** for agents that orchestrate other agents — not the default loop.',
320
+ '**Entry:** `bclaw_work(intent, compact?, budget_tokens?)` · `bclaw_context(kind=memory|execution|board|board_summary|delta)`',
321
+ '**Canonical grammar** (entities: plan, decision, constraint, trap, handoff, runtime_note, candidate, sequence, claim, action, assignment, agent_run): `bclaw_find`, `bclaw_get`, `bclaw_create`, `bclaw_update`, `bclaw_remove`, `bclaw_transition`. Reads accept `budget_tokens` and `project` (cross-project routing — unknown names throw).',
322
+ '**Session/claims:** `bclaw_session_start`, `bclaw_session_end`, `bclaw_claim`, `bclaw_release_claim` · **steps:** `bclaw_add_step`, `bclaw_complete_step`, `bclaw_update_step`, `bclaw_delete_step` · **sequences:** `bclaw_list_sequences`, `bclaw_create_sequence`, `bclaw_update_sequence`, `bclaw_delete_sequence`',
323
+ '**Inbox:** `bclaw_read_inbox`, `bclaw_ack_message`, `bclaw_send_message`, `bclaw_correct_handoff` · **capture:** `bclaw_write_note`, `bclaw_quick_capture(text, type?)` · **search:** `bclaw_search` · **setup:** `bclaw_setup`, `bclaw_bootstrap`, `bclaw_switch`, `bclaw_release_notes`',
324
+ '**Escalation (orchestrators):** `bclaw_coordinate(intent=review|consult|assign|ideate)` · `bclaw_dispatch(intent=execute)` on an active sequence · `bclaw_loop(intent=turn|complete_turn|advance|close)` to drive turns · `bclaw_dispatch_status(target_id)` to verify',
375
325
  '',
376
- '**Entry facades:** `bclaw_work(intent, compact?)`, `bclaw_context(kind)` bclaw_work defaults to compact:true (minimal payload); use compact:false or bclaw_context for full memory',
377
- '**Canonical grammar (standard tier) — your main tool for working with memory:**',
378
- '- `bclaw_find(entity, filter?)` — list by type',
379
- '- `bclaw_get(entity, id)` — read one',
380
- '- `bclaw_create(entity, data)` — add a new plan / decision / constraint / trap / handoff / candidate / runtime_note',
381
- '- `bclaw_update(entity, id, patch)` — edit in place',
382
- '- `bclaw_remove(entity, id, purge?)` — soft-delete (or purge)',
383
- '- `bclaw_transition(entity, id, to)` — change status (e.g. plan todo→in_progress→done)',
384
- '',
385
- 'Entities supported by the grammar: plan, decision, constraint, trap, handoff, runtime_note, candidate, sequence, claim, action, assignment, agent_run.',
386
- '',
387
- '**Cross-project access (pln#359):** every canonical-grammar call, `bclaw_context`, and `bclaw_coordinate` accept an optional `project: <name>` argument that routes the operation to a linked project (cross_project_links from `brainclaw link list` OR a workspace store-chain child). Identity is sourced from the caller; writes + audit land in the target. Unknown project names throw — no silent fallback. The CLI exposes the same as `--project <name>` (mutually exclusive with `--cwd`). Example: `bclaw_get(entity="trap", id="trp#36", project="brainclaw-site")`. Cross-project `bclaw_coordinate` is inbox-only — auto-spawn is force-disabled because the spawn cwd / worktree are tied to the target repo; the target agent picks the brief up async via its own `bclaw_work`.',
388
- '',
389
- '**Session + claims:** `bclaw_session_start`, `bclaw_session_end`, `bclaw_claim`, `bclaw_release_claim`',
390
- '**Plan steps:** `bclaw_add_step`, `bclaw_complete_step`, `bclaw_update_step`, `bclaw_delete_step`',
391
- '**Sequences:** `bclaw_list_sequences`, `bclaw_create_sequence`, `bclaw_update_sequence`, `bclaw_delete_sequence` — create/activate ordered lanes for parallel dispatch. Item shape: `{ planId, stepId?, rank, hard_after?, soft_after?, lane?, scope_hint?, rationale? }`.',
392
- '**Inbox + handoffs:** `bclaw_read_inbox`, `bclaw_ack_message`, `bclaw_send_message`, `bclaw_correct_handoff`',
393
- '**Notes + search:** `bclaw_write_note`, `bclaw_quick_capture`, `bclaw_search`',
394
- '**Escalation (orchestrator path):**',
395
- '- Review / consult / assign another agent → `bclaw_coordinate(intent=review|consult|assign)` (use `open_loop=true` on review to also dispatch the reviewer turn)',
396
- '- Parallel execute across a sequence\'s lanes → create/update an active sequence, then `bclaw_dispatch(intent=analysis)` and `bclaw_dispatch(intent=execute)`',
397
- '- Drive your turn in an already-opened loop → `bclaw_loop(intent=turn|complete_turn|advance|close)`',
398
- '**Setup + navigation:** `bclaw_setup`, `bclaw_bootstrap`, `bclaw_switch`, `bclaw_release_notes`',
399
- '',
400
- 'Legacy per-entity tools (`bclaw_list_plans`, `bclaw_accept`, `bclaw_get_context`, `bclaw_dispatch_review`, …) were removed from the catalog at v1.0 — direct calls still succeed as a migration escape hatch but emit a redirect warning. See `docs/integrations/mcp.md` + `docs/concepts/mcp-governance.md` for the full catalog and stability contract.',
326
+ 'Responses are self-teachingfollow their `next_actions`. Full catalog + stability contract: `docs/integrations/mcp.md`, `docs/concepts/mcp-governance.md`.',
401
327
  ].join('\n');
402
328
  }
403
329
  // ─── Live section renderers ─────────────────────────────────────────────────
@@ -464,7 +390,7 @@ function renderOpenHandoffs(state, limit) {
464
390
  if (handoffs.length === 0)
465
391
  return undefined;
466
392
  return [
467
- '## brainclaw — open handoffs',
393
+ '## brainclaw open handoffs',
468
394
  '',
469
395
  ...handoffs.map((h) => {
470
396
  const plan = h.plan_id ? ` (${h.plan_id})` : '';
@@ -500,20 +426,13 @@ function priorityOrder(priority) {
500
426
  default: return 0;
501
427
  }
502
428
  }
429
+ /**
430
+ * Resolve an agent name to its export format by reading AGENT_EXPORT_REGISTRY.
431
+ * Was a hand-maintained switch that drifted with every new agent — now derived
432
+ * from the same registry the export command iterates, so adding an agent in one
433
+ * place updates every consumer (pln#546 step 2).
434
+ */
503
435
  function formatForAgent(agentName) {
504
- switch (agentName) {
505
- case 'claude-code': return 'claude-md';
506
- case 'cursor': return 'cursor-rules';
507
- case 'github-copilot': return 'copilot-instructions';
508
- case 'opencode':
509
- case 'codex': return 'agents-md';
510
- case 'antigravity': return 'gemini-md';
511
- case 'windsurf': return 'windsurf';
512
- case 'cline': return 'cline';
513
- case 'roo': return 'roo';
514
- case 'kilocode': return 'kilocode';
515
- case 'continue': return 'continue';
516
- default: return 'agents-md';
517
- }
436
+ return resolveExportTarget(agentName).format;
518
437
  }
519
438
  //# sourceMappingURL=instruction-templates.js.map
package/dist/core/io.js CHANGED
@@ -7,6 +7,7 @@ const STORE_LOCK_BASENAME = '.store-mutation';
7
7
  const RETRYABLE_RENAME_ERROR_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
8
8
  const DEFAULT_RENAME_RETRY_ATTEMPTS = 6;
9
9
  const DEFAULT_RENAME_RETRY_DELAY_MS = 25;
10
+ const TMP_ORPHAN_MIN_AGE_MS = 60_000;
10
11
  /**
11
12
  * Entity-aligned directory mapping.
12
13
  * Maps legacy flat directory names to their entity-partitioned paths.
@@ -83,7 +84,9 @@ export function memoryPath(filename, cwd, preferredDirName) {
83
84
  return path.join(memoryDir(cwd, preferredDirName), filename);
84
85
  }
85
86
  export function storeLockPath(cwd, preferredDirName) {
86
- const root = cwd ?? process.cwd();
87
+ // O3 (lop_e2d566765b8b4ce3): canonicalize so two spellings of the same store
88
+ // (relative vs absolute) produce one lock target / re-entrancy key.
89
+ const root = path.resolve(cwd ?? process.cwd());
87
90
  const dirName = preferredDirName ?? MEMORY_DIR;
88
91
  // Keep the store-wide lock alongside the store root so it survives
89
92
  // upgrade park/swap renames. Writers and upgrade/rollback all share
@@ -214,6 +217,32 @@ function makeTempPath(filepath) {
214
217
  const unique = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 10)}`;
215
218
  return path.join(dir, `.${base}.${unique}.tmp`);
216
219
  }
220
+ function isProcessAlive(pid) {
221
+ if (!Number.isInteger(pid) || pid <= 0)
222
+ return false;
223
+ try {
224
+ process.kill(pid, 0);
225
+ return true;
226
+ }
227
+ catch {
228
+ return false;
229
+ }
230
+ }
231
+ function tempOwnerPid(entry) {
232
+ if (!entry.endsWith('.tmp'))
233
+ return undefined;
234
+ const parts = entry.split('.');
235
+ const pid = Number(parts.at(-4));
236
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
237
+ }
238
+ function shouldRemoveTmp(entry, stat) {
239
+ const pid = tempOwnerPid(entry);
240
+ if (!pid)
241
+ return false;
242
+ if (Date.now() - stat.mtimeMs < TMP_ORPHAN_MIN_AGE_MS)
243
+ return false;
244
+ return !isProcessAlive(pid);
245
+ }
217
246
  function isRetryableRenameError(error) {
218
247
  if (!(error instanceof Error) || !('code' in error))
219
248
  return false;
@@ -261,7 +290,14 @@ export function cleanOrphanFiles(dirPath) {
261
290
  try {
262
291
  for (const entry of fs.readdirSync(dirPath)) {
263
292
  const full = path.join(dirPath, entry);
264
- if (entry.endsWith('.tmp') && fs.statSync(full).isFile()) {
293
+ let stat;
294
+ try {
295
+ stat = fs.statSync(full);
296
+ }
297
+ catch {
298
+ continue;
299
+ }
300
+ if (entry.endsWith('.tmp') && stat.isFile() && shouldRemoveTmp(entry, stat)) {
265
301
  try {
266
302
  fs.unlinkSync(full);
267
303
  removed++;
@@ -269,7 +305,7 @@ export function cleanOrphanFiles(dirPath) {
269
305
  catch { /* already gone */ }
270
306
  }
271
307
  // Recurse into subdirectories
272
- if (fs.statSync(full).isDirectory()) {
308
+ if (stat.isDirectory()) {
273
309
  removed += cleanOrphanFiles(full);
274
310
  }
275
311
  }
@@ -57,7 +57,16 @@ export class JsonStore {
57
57
  }
58
58
  }
59
59
  pathFor(id) {
60
- return path.join(this.dirPath, `${id}.json`);
60
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) {
61
+ throw new Error(`Invalid ${this.documentType} id '${id}'`);
62
+ }
63
+ const root = path.resolve(this.dirPath);
64
+ const filepath = path.resolve(root, `${id}.json`);
65
+ const relative = path.relative(root, filepath);
66
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
67
+ throw new Error(`Invalid ${this.documentType} id '${id}'`);
68
+ }
69
+ return filepath;
61
70
  }
62
71
  }
63
72
  //# sourceMappingURL=json-store.js.map
package/dist/core/lock.js CHANGED
@@ -3,16 +3,30 @@ import path from 'node:path';
3
3
  const DEFAULT_TIMEOUT_MS = 5000;
4
4
  const LOCK_RETRY_INTERVAL_MS = 50;
5
5
  const LOCK_EXPIRY_MS = 10000;
6
+ const LOCK_REFRESH_INTERVAL_MS = Math.max(1000, Math.floor(LOCK_EXPIRY_MS / 3));
6
7
  const heldLocks = new Map();
7
8
  function lockFilePath(targetPath) {
8
9
  return targetPath + '.lock';
9
10
  }
11
+ /**
12
+ * Review follow-up O3 (lop_e2d566765b8b4ce3): the re-entrancy map key must be
13
+ * canonical, or two spellings of the same store path (relative vs absolute,
14
+ * Windows drive-letter case) miss each other in `heldLocks` and the nested
15
+ * acquire self-deadlocks for the full timeout. Resolve + case-fold (win32) for
16
+ * the KEY only — the filesystem path used for I/O keeps its original spelling.
17
+ */
18
+ function lockKey(lockPath) {
19
+ const resolved = path.resolve(lockPath);
20
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
21
+ }
10
22
  function syncSleep(ms) {
11
23
  if (ms <= 0)
12
24
  return;
13
25
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
14
26
  }
15
27
  function isProcessAlive(pid) {
28
+ if (!Number.isInteger(pid) || pid <= 0)
29
+ return false;
16
30
  try {
17
31
  process.kill(pid, 0);
18
32
  return true;
@@ -30,43 +44,154 @@ function readLockData(lockPath) {
30
44
  return null;
31
45
  }
32
46
  }
47
+ function randomToken() {
48
+ return `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
49
+ }
50
+ function sameLockData(left, right) {
51
+ if (!left || !right)
52
+ return false;
53
+ return left.pid === right.pid
54
+ && left.timestamp === right.timestamp
55
+ && (left.token ?? '') === (right.token ?? '');
56
+ }
57
+ function lockIsOwnedByCurrentProcess(data, token) {
58
+ return Boolean(data && data.pid === process.pid && data.token === token);
59
+ }
60
+ function writeLockData(lockPath, data, flag) {
61
+ fs.writeFileSync(lockPath, JSON.stringify(data), { encoding: 'utf-8', flag });
62
+ }
33
63
  function tryCreateLock(lockPath) {
34
- const data = { pid: process.pid, timestamp: Date.now() };
64
+ const token = randomToken();
65
+ const data = { pid: process.pid, timestamp: Date.now(), token };
35
66
  try {
36
- fs.writeFileSync(lockPath, JSON.stringify(data), { encoding: 'utf-8', flag: 'wx' });
37
- return true;
67
+ writeLockData(lockPath, data, 'wx');
68
+ return token;
38
69
  }
39
70
  catch (err) {
40
71
  if (err instanceof Error && 'code' in err) {
41
72
  const code = err.code;
42
73
  if (code === 'EEXIST' || code === 'EPERM' || code === 'EACCES') {
43
- return false;
74
+ return null;
44
75
  }
45
76
  }
46
77
  throw err;
47
78
  }
48
79
  }
49
- function tryBreakLock(lockPath) {
50
- const data = readLockData(lockPath);
80
+ function lockFileIsOld(lockPath) {
81
+ try {
82
+ const stat = fs.statSync(lockPath);
83
+ return Date.now() - stat.mtimeMs > LOCK_EXPIRY_MS;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ function canBreakLock(lockPath, data) {
51
90
  if (!data)
91
+ return lockFileIsOld(lockPath);
92
+ if (data.pid === process.pid)
52
93
  return false;
53
- const expired = Date.now() - data.timestamp > LOCK_EXPIRY_MS;
54
- const ownerDead = !isProcessAlive(data.pid);
55
- if (!expired && !ownerDead)
94
+ if (isProcessAlive(data.pid))
95
+ return false;
96
+ return true;
97
+ }
98
+ /**
99
+ * Review follow-up O2 (lop_e2d566765b8b4ce3) — accepted residual risk: a
100
+ * microsecond TOCTOU remains between the second read and the rename, where a
101
+ * third process can break+reacquire and the renamer steals a live lock. The
102
+ * restore branch covers it unless a FOURTH process acquires inside that gap.
103
+ * Decision (sprint 1.5): not closing it. The full fix is an O_EXCL token
104
+ * handshake protocol — a coordinated change to every lock consumer — and the
105
+ * residual window requires 4 racing processes inside microseconds on a lock
106
+ * whose owner is already dead. Strictly better than the old unlink-based break.
107
+ */
108
+ function tryBreakLock(lockPath) {
109
+ const observed = readLockData(lockPath);
110
+ if (!canBreakLock(lockPath, observed))
111
+ return false;
112
+ const current = readLockData(lockPath);
113
+ if (!sameLockData(observed, current) && (observed || current))
114
+ return false;
115
+ if (!canBreakLock(lockPath, current))
116
+ return false;
117
+ // Dot-separated suffix with pid 4th-from-end so cleanOrphanFiles/tempOwnerPid
118
+ // can reclaim tombstones from crashed breakers (dash-separated names were
119
+ // unparseable and accumulated forever).
120
+ const tombstone = `${lockPath}.stale.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
121
+ try {
122
+ fs.renameSync(lockPath, tombstone);
123
+ }
124
+ catch {
56
125
  return false;
126
+ }
57
127
  try {
58
- fs.unlinkSync(lockPath);
128
+ const moved = readLockData(tombstone);
129
+ if ((observed || moved) && !sameLockData(observed, moved)) {
130
+ try {
131
+ if (!fs.existsSync(lockPath))
132
+ fs.renameSync(tombstone, lockPath);
133
+ }
134
+ catch {
135
+ // If another process already acquired the lock path, leave the
136
+ // mismatched tombstone for orphan cleanup instead of deleting live data.
137
+ }
138
+ return false;
139
+ }
140
+ const token = tryCreateLock(lockPath);
141
+ try {
142
+ fs.unlinkSync(tombstone);
143
+ }
144
+ catch { /* best effort */ }
145
+ if (token) {
146
+ startHeldLock(lockPath, token);
147
+ return true;
148
+ }
149
+ return false;
59
150
  }
60
151
  catch {
152
+ try {
153
+ if (!fs.existsSync(lockPath))
154
+ fs.renameSync(tombstone, lockPath);
155
+ }
156
+ catch {
157
+ // Best effort recovery; acquisition will retry.
158
+ }
61
159
  return false;
62
160
  }
63
- return tryCreateLock(lockPath);
161
+ }
162
+ function refreshLock(lockPath, token) {
163
+ const current = readLockData(lockPath);
164
+ if (!lockIsOwnedByCurrentProcess(current, token))
165
+ return;
166
+ try {
167
+ // 'r+' writes in place without truncating. Pad with trailing spaces (valid
168
+ // JSON whitespace) so a payload shorter than the on-disk file can never
169
+ // leave trailing garbage that would corrupt the lock for readers.
170
+ let payload = JSON.stringify({ ...current, timestamp: Date.now() });
171
+ try {
172
+ const existingSize = fs.statSync(lockPath).size;
173
+ if (Buffer.byteLength(payload, 'utf-8') < existingSize) {
174
+ payload = payload.padEnd(payload.length + (existingSize - Buffer.byteLength(payload, 'utf-8')), ' ');
175
+ }
176
+ }
177
+ catch { /* stat failed — write unpadded */ }
178
+ fs.writeFileSync(lockPath, payload, { encoding: 'utf-8', flag: 'r+' });
179
+ }
180
+ catch {
181
+ // A failed refresh is not fatal; contenders still respect pid liveness.
182
+ }
183
+ }
184
+ function startHeldLock(lockPath, token) {
185
+ const refreshTimer = setInterval(() => refreshLock(lockPath, token), LOCK_REFRESH_INTERVAL_MS);
186
+ refreshTimer.unref?.();
187
+ heldLocks.set(lockKey(lockPath), { count: 1, token, refreshTimer });
64
188
  }
65
189
  export function acquireLock(targetPath, timeoutMs = DEFAULT_TIMEOUT_MS) {
66
190
  const lockPath = lockFilePath(targetPath);
67
- const heldCount = heldLocks.get(lockPath);
68
- if (heldCount) {
69
- heldLocks.set(lockPath, heldCount + 1);
191
+ const held = heldLocks.get(lockKey(lockPath));
192
+ if (held) {
193
+ held.count += 1;
194
+ refreshLock(lockPath, held.token);
70
195
  return true;
71
196
  }
72
197
  const deadline = Date.now() + timeoutMs;
@@ -75,12 +200,12 @@ export function acquireLock(targetPath, timeoutMs = DEFAULT_TIMEOUT_MS) {
75
200
  fs.mkdirSync(dir, { recursive: true });
76
201
  }
77
202
  while (Date.now() < deadline) {
78
- if (tryCreateLock(lockPath)) {
79
- heldLocks.set(lockPath, 1);
203
+ const token = tryCreateLock(lockPath);
204
+ if (token) {
205
+ startHeldLock(lockPath, token);
80
206
  return true;
81
207
  }
82
208
  if (tryBreakLock(lockPath)) {
83
- heldLocks.set(lockPath, 1);
84
209
  return true;
85
210
  }
86
211
  syncSleep(Math.min(LOCK_RETRY_INTERVAL_MS, deadline - Date.now()));
@@ -89,14 +214,16 @@ export function acquireLock(targetPath, timeoutMs = DEFAULT_TIMEOUT_MS) {
89
214
  }
90
215
  export function releaseLock(targetPath) {
91
216
  const lockPath = lockFilePath(targetPath);
92
- const heldCount = heldLocks.get(lockPath);
93
- if (heldCount && heldCount > 1) {
94
- heldLocks.set(lockPath, heldCount - 1);
217
+ const held = heldLocks.get(lockKey(lockPath));
218
+ if (held && held.count > 1) {
219
+ held.count -= 1;
95
220
  return;
96
221
  }
97
- heldLocks.delete(lockPath);
222
+ heldLocks.delete(lockKey(lockPath));
223
+ if (held?.refreshTimer)
224
+ clearInterval(held.refreshTimer);
98
225
  try {
99
- if (fs.existsSync(lockPath)) {
226
+ if (lockIsOwnedByCurrentProcess(readLockData(lockPath), held?.token ?? '')) {
100
227
  fs.unlinkSync(lockPath);
101
228
  }
102
229
  }
@@ -130,13 +257,11 @@ export function cleanStaleLocks(dirPath) {
130
257
  continue;
131
258
  const lockPath = path.join(dirPath, entry);
132
259
  const data = readLockData(lockPath);
133
- if (!data)
134
- continue;
135
- const expired = Date.now() - data.timestamp > LOCK_EXPIRY_MS;
136
- const ownerDead = !isProcessAlive(data.pid);
137
- if (expired || ownerDead) {
260
+ if (canBreakLock(lockPath, data)) {
261
+ const tombstone = `${lockPath}.clean.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
138
262
  try {
139
- fs.unlinkSync(lockPath);
263
+ fs.renameSync(lockPath, tombstone);
264
+ fs.unlinkSync(tombstone);
140
265
  removed++;
141
266
  }
142
267
  catch {
@@ -18,8 +18,10 @@
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import { acquireClaimScope, listClaims, releaseClaim } from '../claims.js';
21
+ import { buildSurveySignalsBaseline } from './hooks/survey-signals-baseline.js';
21
22
  import { BOOTSTRAP_PRESET } from './presets/bootstrap.js';
22
23
  import { listLoops, openLoop } from './store.js';
24
+ import { add_artifact } from './verbs.js';
23
25
  export class BootstrapCoordinationInProgressError extends Error {
24
26
  blockingClaimId;
25
27
  constructor(blockingClaimId) {
@@ -165,7 +167,7 @@ export function acquireBootstrapLoop(opts, cwd) {
165
167
  // Step 3 — open the loop, release the lock.
166
168
  const lockClaimId = acquireResult.claim.id;
167
169
  try {
168
- const loop = openLoop({
170
+ let loop = openLoop({
169
171
  kind: 'ideation',
170
172
  title: opts.title ?? 'Bootstrap PROJECT.md',
171
173
  goal: opts.goal,
@@ -181,6 +183,28 @@ export function acquireBootstrapLoop(opts, cwd) {
181
183
  stop_condition: BOOTSTRAP_PRESET.stop_condition,
182
184
  protocol: BOOTSTRAP_PRESET.protocol,
183
185
  }, cwd);
186
+ // pln#557 step 4 — seed the survey phase with the deterministic-scanner
187
+ // baseline so survey quality is reproducible across champions instead of
188
+ // depending on per-agent re-discovery (TranslaVox miss, can_0160d6c4).
189
+ // Attached as `signals_baseline` (freeform body), NOT `signals_report`,
190
+ // so the survey advance-gate is not auto-traversed: the champion enriches
191
+ // the baseline into its own signals_report. Best-effort — a scanner
192
+ // failure must never block opening the loop.
193
+ try {
194
+ const baseline = buildSurveySignalsBaseline(cwd ?? process.cwd());
195
+ loop = add_artifact({
196
+ id: loop.id,
197
+ actor: opts.created_by ?? opts.agent_id ?? opts.actor,
198
+ artifact: {
199
+ phase: 'survey',
200
+ type: 'signals_baseline',
201
+ body: JSON.stringify(baseline),
202
+ produced_by: 'brainclaw-scanner',
203
+ },
204
+ }, cwd);
205
+ warnings.push('survey baseline attached (artifact type signals_baseline, produced by the deterministic scanner) — enrich it into your signals_report instead of re-scanning the repo from scratch.');
206
+ }
207
+ catch { /* best-effort */ }
184
208
  return { action: 'opened', loop, warnings };
185
209
  }
186
210
  finally {
@@ -58,6 +58,8 @@ export const BclawLoopTurnSchema = z.object({
58
58
  role: z.string().optional(),
59
59
  input: z.string().optional(),
60
60
  assignment_id: z.string().optional(),
61
+ /** pln#562 step 4 — claim binding the turn's slot to a dispatched instance. */
62
+ claim_id: z.string().optional(),
61
63
  dispatch: z.boolean().optional(),
62
64
  expected_version: z.number().int().nonnegative().optional(),
63
65
  ...CallerEnvelopeFields,