brainclaw 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -429,258 +429,12 @@ npm run test:coverage # with coverage report
429
429
 
430
430
  ## Changelog
431
431
 
432
- For older releases (v0.x and the early v1.0 launch series), `git log` on `master` is the source of truth — every release commit follows the `chore(release): bump version to <semver>` convention, and the matching feature/fix commits reference their plan id (e.g. `feat(mcp): self-heal ... (pln#478)`).
433
-
434
- ### v1.13.0
435
-
436
- Operator-maturity batch from two days of heavy multi-agent dogfooding — dispatch/worktree lifecycle, claim parity, write-path auto-repair, model routing, benchmark gate, faster context reads:
437
-
438
- - **Model selection for codex and copilot spawns** (pln#606) — `dispatch run --model <m>` now reaches codex (`exec -m`) and copilot argv, verified on the installed binaries. The codex `--add-dir` writable-roots spike closed negative on Windows: file-first stays the codex transport.
439
- - **Auto-repair identity & session on canonical writes** (pln#608) — `bclaw_create`/`update`/`remove`/`transition` auto-register + auto-session instead of throwing "Start a session first", with an explicit `session auto-created` warning; the trust boundary for unknown identities is unchanged.
440
- - **Time-to-first-value benchmark with blocking CI budgets** (pln#604) — seeded synthetic stores, three scenarios (cold_onboard / warm_work / first_edit), budgets versioned in `bench-budgets.json`.
441
- - **Claim lifecycle parity** (trp#928) — `bclaw_transition(entity='claim')` wired, `coordinator_override` (trusted+) on release/transition, cascade release with per-claim logging on plan-done / loop close / assignment-completed / harvest, entity-scoped `bclaw_find` filter rejections.
442
- - **Squash-aware worktree GC + junction-safe removal** (trp#926) — squash-merged lanes are finally collected (content-based detection), and the Windows `node_modules`-junction wipe class is closed for good; `dispatch_status` compares against the worktree's creation ref.
443
- - **VS Code extension** — Backlog pagination (recent plans were invisible, trp#925) and probe/spawn parity with classified, actionable resolver failures (trp#927).
444
- - **Context read 2× faster on large stores** (pln#578) — 3 of the 4 full-store read passes per context build eliminated (disabled-reputation sweep ×2, estimation reload, triple candidate scan): 23.9 s → 11.9 s, 196 MB → 49 MB parsed, byte-identical output.
445
-
446
- ### v1.12.0
447
-
448
- Auto-localized execution writes for multi-project workspaces (pln#597), from DGX-Spark dogfooding:
449
-
450
- - **Execution writes auto-localize into a workspace sibling named by `project=X`** — `bclaw_create`/`bclaw_transition` (plan & claim), `bclaw_claim`, the step tools and `bclaw_delete_plan` open a session + sticky switch into a workspace store-chain child instead of rejecting with "limited to signaling entities", echoing `auto_switched`. The signaling-vs-execution boundary is re-scoped to federation (`cross_project_links`): federated links and unknown names stay blocked.
451
-
452
- ### v1.11.1
453
-
454
- Agent-identity & session-hook resilience, from a fresh-CLI dogfood on a monorepo:
455
-
456
- - **Session hooks no longer spam `UserPromptSubmit hook error` on every prompt.** Root cause was an agent-identity error whose own remediation (`register-agent --set-current`) the resolver ignored, swallowed by `2>/dev/null`. Now: the error hint points at what actually works (`--agent` / `$BRAINCLAW_AGENT_NAME`), a **single registered agent auto-resolves** with no env signal, and `session-start`/`context-diff`/`session-end` run with `--hook` so they degrade to exit 0 + `~/.brainclaw/hook.log` instead of erroring the prompt loop. Multi-agent safety (pln#562) is unchanged.
457
- - **`brainclaw doctor --fix-hooks`** purges stale/broken/duplicate brainclaw hooks across all Claude Code settings scopes (user + cwd) and rewrites the canonical ones — for the broken hooks that accumulate where `setup`'s git-repo discovery never reached.
458
- - **`setup` repo discovery recurses** (bounded, skipping `node_modules`/build dirs) so repos nested deep in a workspace are found instead of silently missed, and it registers the detected agent so the hooks it installs can resolve an identity.
459
-
460
- ### v1.11.0
461
-
462
- Monorepo project-resolution + Code Map, cross-project relocation, and dispatch-worktree hygiene — from a multi-project (monorepo) dogfood:
463
-
464
- - **`bclaw_switch` to the monorepo root now works, and a switch is honored everywhere.** An agent inside a child project could not switch *up* to the workspace-root project, and a session-scoped switch was silently lost (sessions are stored per-cwd, but the resolver read them only at the workspace anchor). The resolver now matches the root project by `project_name`/`project_id` and probes the anchor / physical cwd / workspace root for the session, so `bclaw_switch` is authoritative for every subsequent call — including Code Map — without `--cwd`.
465
- - **`code-map refresh --cascade`** — monorepo-native, per-project indexing: one command at the root refreshes **every** nested project into its own store plus a child-scoped root store, with **zero double-indexing** (even under nesting). `code-map status --cascade` adds a per-child recap. Opt-in; single-project repos are unchanged.
466
- - **`brainclaw move <entity> <id> --to <project>` / `bclaw_move`** — id-preserving cross-project relocation, closing the gap the switch bug exposed (items created in the wrong store). Execution-local entities (claim/assignment/agent_run/session) stay put; refuses id collisions and active-claim moves; audits both stores.
467
- - **Sub-agent worktrees auto-clean on loop close** — a completed review/dispatch loop garbage-collects its worktrees (junction-safe; keeps any that are alive, un-harvested, or carry unmerged commits). Opt out with `BRAINCLAW_NO_WORKTREE_GC=1`.
468
-
469
- ### v1.10.2
470
-
471
- Dispatch worktree-creation hardening (parallel-lane dispatch on a large repo): the claim-scope branch slug is length-capped *before* its trailing-dot strip (a scope ending in `…Page.astro` no longer truncates to an invalid `feat/…Page.` ref), and `git worktree add` gets a dedicated timeout (120s default; override `BRAINCLAW_WORKTREE_ADD_TIMEOUT_MS`) so a several-hundred-file checkout isn't SIGTERM-killed mid-materialize.
472
-
473
- ### v1.10.1
474
-
475
- Code Map fast-follows from the 1.10.0 dogfood, plus a lint cleanup:
476
-
477
- - A git branch switch now flags the index stale (new `stale_git_head` freshness reason); `refresh` honors `.gitignore` (gitignored output dirs no longer indexed); `brief("<path>")` resolves the exact file instead of token-flooding; and the freshness badge distinguishes index freshness from a call's spot-check (`index_status`).
478
- - The `brainclaw-session` skill + agent instruction surface now point agents at `bclaw_code_find` / `bclaw_code_brief` before grep. Lint baseline cleared to zero (stylistic rules ratcheted to `error`); README doc links clickable; `docs/code-map.md` lists JS / TS / JSX / TSX · Python · PHP · Java.
479
-
480
- ### v1.10.0
481
-
482
- - **Code Map** — a new per-project [Tree-sitter](docs/code-map.md) symbol + import
483
- index for **JS / TS / TSX, Python, PHP, and Java**. Ask *"where is X / what should I
484
- read first"* before editing: `bclaw_code_find`, `bclaw_code_brief`,
485
- `bclaw_code_status`, `bclaw_code_refresh` (CLI: `brainclaw code-map …`). Pull-based
486
- freshness with a per-response badge; never blocks `bclaw_work`; monorepo-aware. See
487
- [code map](docs/code-map.md).
488
- - **Monorepo multi-project safety** — agents working in different child projects of a
489
- monorepo are now genuinely independent:
490
- - an anchored agent working *inside* a child project resolves **that** child, not the
491
- repo root — no more "plans / index target the repo root";
492
- - CLI `brainclaw switch` is **session-scoped by default** (two agents no longer clobber
493
- a shared pointer); the new `--global` flag is the only path that sets the shared
494
- workspace default; `switch --list` / show are session-aware;
495
- - a session-less agent physically inside a child project beats a stale shared global
496
- pointer (resolves the child it is in);
497
- - dispatched / CoDev workers spawn with a **clean identity** — the coordinator's
498
- session / project / agent env no longer leaks into a worker.
499
- - Internal — MCP public surface fingerprint re-pinned for the Code Map tools (additive;
500
- no tool removed or renamed).
501
-
502
- ### v1.9.1
503
-
504
- - **Multi-project scoping fixes for monorepos** — MCP reads/writes resolve the
505
- effective project from an explicit arg, the MCP session, `BRAINCLAW_CWD`, or
506
- the global active project (in that order), so commands target the right store
507
- inside a monorepo; `bclaw_find` legacy-filter diagnostics report how many
508
- `provenance.kind="legacy"` records the default filter excluded.
509
- - **Case-insensitive agent names** — `targetAgents=["Codex"]` (any casing) now
510
- resolves like `codex`; dispatch/coordinate no longer silently drop a reviewer
511
- over a capital letter, and an unresolved name reports a distinct `unknown_agent`
512
- reason instead of the misleading "no CLI spawn template (IDE-only?)".
513
- - **Docs** — revamped Loop Engine, Git Worktrees, Cross-Project Signals, and the
514
- Orchestration Playbook, corrected against actual behavior (`bclaw_work` =
515
- session + context + claim, not worktree provisioning; worktrees come from
516
- dispatch/coordinate; plans via `bclaw_create(entity="plan")`).
517
-
518
- ### v1.9.0
519
-
520
- - **Release hardening for npm publishing + agent-surface coherence** (pln#571).
521
- A dedicated CI **Release Package Check** builds the CLI, builds the optional
522
- VS Code extension, runs the extension tests, and verifies the published tarball
523
- contents; `version --publish-local`/`prepublishOnly` now run `build:release` +
524
- `pack:check` before `npm pack`. The `.vsix` ships as an *optional* IDE
525
- companion (local CLI builds skip it; release builds stay strict). Generated
526
- agent surfaces no longer pin the npm semver (stable `Managed by brainclaw`
527
- banner), and `.brainclaw/project.md` is documented as a legacy derived view —
528
- root `PROJECT.md` is the durable project vision; live claims/plans/handoffs
529
- live in `agent-board` / MCP context.
530
- - **Coordination & safety fixes** — strict claim isolation (a duplicate active
531
- claim on the same scope now fails inside the mutation lock instead of warning),
532
- `JsonStore` entity-id validation to prevent path traversal (pln#571), and
533
- `reflect --batch` no longer swallows identity/security errors so a strict
534
- import can never silently skip sensitive content (pln#572).
535
- - **Concurrency & CI robustness** — the journal append waits fairly under
536
- contention so a multi-process kill-9 storm converges without seq reuse
537
- (pln#573/#574), and `merge-risk` reconciles Windows 8.3 short-name worktree
538
- paths so a claim matches its lane on Windows runners (pln#576).
539
- - **Docs** — Roo, Continue, Windsurf, Copilot, OpenClaw, quickstart, and the
540
- agent-integration docs now match the current export formats and setup behavior
541
- (pln#571/#575).
542
-
543
- ### v1.8.0
544
-
545
- - **Multi-agent dispatch convergence — "worktree-as-contract"** (from a real
546
- cross-project field session where a sandboxed worker could neither commit nor
547
- reach MCP). The worker's contract shrinks to "edit files in this worktree +
548
- drop `LANE-RESULT.json`": `brainclaw harvest --integrate` commits the worktree
549
- diff on behalf of a worker that can't self-commit (hard-guarded to the linked
550
- worktree, never the main repo), then completes the assignment and releases the
551
- claim with plan cascade. A `LANE-RESULT.json` is now the #1 verdict signal in
552
- `bclaw_dispatch_status` (worker FINISHED, even without self-update); the
553
- dispatcher refuses to spawn without an isolated worktree; `open_loop` reviews
554
- pre-flight each reviewer agent with a trivial validation spawn (clear
555
- boot-failure reason instead of a generic loop timeout); and decisions/traps
556
- gain `verified_at`/`verify_cmd` so perishable facts can be flagged stale.
557
- Additive + opt-in throughout. (pln#530, pln#531, pln#532, pln#533, pln#534, trp#468)
558
-
559
- ### v1.7.5
560
-
561
- - **Security patch (recommended upgrade)** — fixes a git command-injection / RCE
562
- vector flagged by Socket AI: several commands interpolated a git ref (notably
563
- one derived from the persisted session `git_sha`) into `execSync` shell
564
- strings. All git calls now use `execFileSync` (no shell) and `git_sha` is
565
- validated as a hex SHA. No functional change. (session-end, release-claims,
566
- release-notes, sync)
567
-
568
- ### v1.7.4
569
-
570
- - **Dispatch observability + worker DX hardening** (from a real cross-project
571
- field session) — `bclaw_dispatch_status` and the reconciler now derive liveness
572
- from filesystem activity (log + worktree mtime), so a worker actively editing
573
- files is no longer falsely flagged `stalled`, and known codex boot-failure
574
- stderr signatures get a targeted diagnosis; briefs are transport-aware (a
575
- sandboxed agent without MCP/commit gets the file protocol, not instructions it
576
- can't follow), backed by a derived capability matrix
577
- (`dispatchHasMcp`/`dispatchCanCommit`); `bclaw_claim` gains an advisory
578
- (no-worktree) mode; `bclaw_find` payloads are size-bounded with pagination
579
- metadata; an opt-in per-worktree `tsc --noEmit` pre-commit gate; gated ready
580
- lanes carry a code-propagation advisory; the reconciler auto-releases the claim
581
- of a run it infers failed; and `plan.related_paths` is now updatable.
582
- (pln#479, pln#491, pln#527, pln#528, pln#529, trp#291, trp#431, trp#433, trp#434)
583
-
584
- ### v1.7.3
585
-
586
- - **Multi-agent dispatch hardening for JS/TS monorepos** — dispatched worktrees
587
- junction-link per-package `node_modules` (npm / yarn / pnpm workspaces), not
588
- just the root, and surface failed links instead of swallowing them; `brainclaw worktree clean` now garbage-collects merged worktrees past birth-noise instead
589
- of skipping them all; the agent inventory reports an agent `spawnable` when its
590
- binary is on PATH even if `--version` is slow to start; dispatch-verification
591
- guidance leads with `bclaw_dispatch_status` (not the untrustworthy Windows
592
- wrapper pid); and a new `LANE-RESULT.json` convention + `brainclaw harvest <assignment_id>` give workers a standard, MCP-free result channel. The dispatch
593
- dirty-guard also ignores `.claude/`, `.cursor/`, and `.codex/` agent-local
594
- config. (pln#523, pln#524, pln#525, pln#526, trp#371, trp#427, trp#428)
595
-
596
- ### v1.7.2
597
-
598
- - **Sequence MCP tools are agent-first by default** — sequence creation,
599
- listing, update, and deletion tools are now in the default MCP catalog, with
600
- explicit lane item schemas (`planId`, optional `stepId`, `rank`,
601
- dependencies, lane metadata) and matching canonical CRUD validation for
602
- `entity="sequence"`.
603
-
604
- ### v1.7.1
605
-
606
- - **MCP project context isolation fix** — `bclaw_switch` now keeps MCP switches
607
- session-scoped even when the agent session has to be resolved or created on
608
- the fly. Session lookup honors explicit session IDs, avoids adopting another
609
- live process's session, detects Codex via native `CODEX_*` runtime variables,
610
- and `bclaw_switch(list=true)` reports the session active project with
611
- `active_source`.
612
-
613
- ### v1.7.0
614
-
615
- - **Dispatch reliability + scope-aware dirty guard** — evidence-first
616
- `agent_run` reconciliation avoids false terminal states, `bclaw_coordinate`
617
- accepts pinned refs and a scope-aware `allow_dirty` guard, and the Hermes
618
- agent integration joins the supported surfaces.
619
-
620
- ### v1.6.0
621
-
622
- - **Bootstrap loop + cross-project agent workflow** — the bootstrap ideation
623
- preset can materialize `PROJECT.md`, `bclaw_init_project` initializes and links
624
- arbitrary project paths, and `project=` routing reaches `bclaw_work` /
625
- `bclaw_loop` for linked-project operations.
626
-
627
- ### v1.5.3
628
-
629
- - **Cross-project canonical grammar + CLI parity** (pln#359, all phases) — the canonical grammar (`bclaw_find / get / create / update / remove / transition`), `bclaw_context`, and `bclaw_coordinate` now accept an optional `project: <name>` argument that routes the operation to a linked project. Two link kinds are recognised: `cross_project_links` (sibling/peer projects in `config.yaml`, `brainclaw link list`) and workspace store-chain children. Arbitrary directory paths are rejected — adoption requires an explicit link, which gives the user a single point of control over what an agent can reach. Identity is sourced from the caller's home registry; entity writes + audit log entries land in the target. Unknown project names throw `validation_error` with a hint listing the configured links — no silent fallback. Cross-project `bclaw_coordinate` is **inbox-only**: claim/assignment/message all land in the target, the target agent picks the brief up async via its own `bclaw_work`, and auto-spawn from the source process is force-disabled because the spawn cwd / worktree are tied to the target's git repo (a warning surfaces in `FacadeResponse.warnings`). The CLI exposes the same as a global `--project <name>` flag, mutually exclusive with `--cwd`. Refs: helper `resolveProjectCwd` in `src/core/cross-project.ts`, MCP write/read handler dispatch in `src/commands/mcp.ts` and `src/commands/mcp-read-handlers.ts`, `--project` plumbing in `src/cli.ts` preAction, surface advertisement in `src/core/instruction-templates.ts`, plus tests in `tests/unit/cross-project.test.ts` (10 unit cases on the helper), `tests/unit/bclaw-coordinate.test.ts` (4 cross-project routing cases), and `tests/cli-cross-project.test.ts` (5 e2e cases). Closes the `--cwd` workaround pattern that had been the day-to-day shape of multi-project sessions.
630
- - **Site facts contract** (umbrella `pln_7fdfd70d` sprint 0) — new `scripts/emit-site-facts.mjs` emits `dist/facts.{js,json}` from `MCP_TOOL_NAMES` + `ENTITY_NAMES` so the brainclaw-site (and any consumer) can pull live tool/entity counts at build time without forking the values into a hand-maintained config. The package `files` list ships `dist/facts.json`; build:cli runs the emitter as part of the chain.
631
-
632
- ### v1.5.2
633
-
634
- - **Grammar fix: `bclaw_update` no longer silently drops patches** (trp#187, pln#500) — the canonical CRUD surface promised that `EntityRegistry.updatable` listed every patchable field, but the legacy impls behind the dispatch (`updatePlan`, `updateMemoryItem`) only handled a typed subset. A `as UpdatePlanInput` cast in `entity-operations.ts` masked the gap from TypeScript: callers passed `{text, tags, severity, …}`, validation accepted them, and the function bodies never read them. Empirically reproduced this session on `pln#359` (text+tags drop) and `trp#187` itself (severity drop). Fix adds a generic-patch escape-hatch (`patch?: Partial<…>`) on `UpdatePlanInput` and `UpdateMemoryInput`, applied via `Object.assign` after the typed fields so legacy CLI callers keep their behaviour. The dispatch in `entity-operations.ts` now passes the full patch through that escape-hatch for plan/decision/constraint/trap. `runtime_note`, `candidate`, and `cross_project_link` were already correct. New `tests/unit/entity-update-coverage.test.ts` is parametric: for every `(entity, field)` pair in `EntityRegistry.updatable`, create + patch + get + assert the field actually persisted (29 cases). Adding a new updatable field will require adding a coverage row, so the contract stays honest.
635
-
636
- ### v1.5.1
637
-
638
- - **Cross-project link CLI + grammar parity** (pln#454 step 2) — new `brainclaw link add/list/remove` subcommands and a stateless `cross_project_link` entity wired into the canonical CRUD verbs (`bclaw_create / find / get / update / remove`). The pain point was small (config.yaml is hand-editable) but the asymmetry — runtime cross-project signaling shipped, management UX did not — meant federation peers had no first-class affordance. Storage stays in `config.cross_project_links`; the entity is intentionally stateless (no transitions). Refs: `src/commands/link.ts`, `src/core/cross-project.ts` (new `addCrossProjectLink` / `removeCrossProjectLink`), `src/core/entity-registry.ts` (new `xpl` prefix), `src/core/entity-operations.ts` dispatch cases, `tests/unit/link.test.ts` + `tests/unit/cross-project-link-grammar.test.ts` (26 new cases).
639
- - **Agent registry pruned** (pln#454 step 1) — 8 synthetic profiles (antigravity, copilot-vscode, claude-opus, copilot-identity, copilot, claude-sonnet, visionnaire, copilot-codev) and 11 abandoned sessions archived under `.brainclaw/archive/agents/2026-05-07/`. The whitelist that survives matches `feedback_allowed_dispatch_agents.md`: `claude-code`, `codex`, `github-copilot`, plus `jberdah` (curator). Auto-re-registration in `detectAiAgent` is intentionally preserved — if a real surface starts a session, the registry auto-rebuilds for it. Pure store cleanup, no code change.
640
- - **Inbox + assignment lifecycle reconciliation** (housekeeping) — 89 stale inbox messages and 7 orphan offered-assignments closed with retroactive lifecycle updates. The plans they backed (pln#478 mcp self-heal, pln#480 multi-stack worktree, pln#483 bclaw_work compact, pln#471 tier-B live companions) had all merged weeks earlier without the dispatch path calling `bclaw_assignment_update`. Captured as `feedback_dispatch_test_inbox_debris` so future drift is mass-acked instead of triaged per-message.
641
-
642
- ### v1.5.0
643
-
644
- - **Ideation loop MVP** (pln#492) — new `bclaw_coordinate(intent='ideate')` opens a memory-confrontation loop: critic reads only adversarial categories (traps + feedback + runtime_notes + critique_history) and gets a BM25-ranked, context-filtered, 12k-token-capped brief assembled from project memory. Single-agent (champion drives manually) or multi-agent (auto-dispatch a turn per critic). Iteration block (cycle, max_iterations, exit_when), phase-advance gate (≥3 critique artifacts before leaving critique), system events (`phase_advance_blocked`, `max_iterations_reached`). Full design: [docs/concepts/ideation-loop.md](docs/concepts/ideation-loop.md).
645
- - **Reliable dispatch + autonomy contract** (pln#496) — codex / sandboxed task-based agents now emit the full lifecycle (accepted → started → progress → completed). `briefMode` resolution corrected: `task-based && hasMcp → 'full'` (not 'compact', which silently dropped the protocol section). Lazy reconciliation pattern: `agentrun-reconciler` runs at every read path (`bclaw_assignment_events`, `bclaw_loop intent='get'`, `doctor --dispatch`) instead of a daemon; first run auto-recovered 9 historical orphan agent_runs in <1s. New autonomy contract section in instruction surfaces — agents execute protocol-defined transitions instead of pausing to ask. New `buildClaimEnvPrefix` consolidates cross-shell env injection (Windows defaults to cmd, POSIX uses unquoted bytes per PATH conventions). New `brainclaw doctor --dispatch` for operator-facing dispatch health.
646
- - **Worktree junction wipe fix** (pln#498) — `detachWorktreeJunctions` runs before `git worktree remove` on Windows so git's recursive rm cannot follow the `node_modules` junction back into the main repo. Closes the recurring post-merge wipe trap; validated empirically on multiple back-to-back merges in the v1.5.0 session.
647
- - **Methodological lessons** captured as user auto-memory feedback memos: bisect historical state before bisecting code (regression vs. always-broken-but-hidden), lazy reconcile at read paths beats daemon polling, codex briefs MUST include the Protocol section (implicit instructions don't propagate to sandboxed CLIs).
648
-
649
- ### v1.2.0
650
-
651
- - **zod 3 → 4 migration** (pln#486) — schemas are semantically equivalent but the JSON Schema emitted by `tools/list` shifted shape; downstream MCP clients that snapshot schemas should re-pin. See `docs/mcp-schema-changelog.md` for the public surface fingerprint.
652
-
653
- ### v1.1.0
654
-
655
- - **Node 20+ baseline** (pln#485) — `engines.node` is now `>=20.0.0` (Node 18 reached EOL in April 2025). CI matrix runs Node 22 and 24 on Linux; Windows on Node 24. Node 20 remains the minimum installable runtime but is no longer CI-verified.
656
- - **commander 13 → 14** (requires Node 20+).
657
- - **@types/node 22 → 24** (LTS-aligned).
658
-
659
- ### v1.0.15
660
-
661
- - **TypeScript 5.8 → 6.0** (pln#484) — migration to `module: "nodenext"` (`Node16` is deprecated in TS 6, scheduled for removal in TS 7); explicit `types: ["node"]` since TS 6 changed the default to `[]`.
662
-
663
- ### v1.0.14
664
-
665
- - **`bclaw_work` compact payload by default** (pln#483) — avoids exceeding the ~25k MCP token cap on projects with substantial memory. Pass `compact: false` for the full payload, or call `bclaw_context(kind="memory")` after.
666
- - **MCP runtime self-heal + `doctor --repair`** (pln#478) — when `dist/mcp-worker.js` is missing, the server logs an actionable repair pointer and read-only handlers keep serving in-process. `brainclaw doctor --repair` rebuilds dist in one step.
667
- - **Tier B/C native live companions** (pln#471) — `.cursor/live.md`, `.clinerules/live.md`, `.windsurf/rules/live.md`, `.github/copilot-instructions.live.md`, `.continue/live.md`, `GEMINI.live.md` regenerated on session-end and handoff. Opt-in via `brainclaw export --include-live --write`.
668
-
669
- ### v1.0.13
670
-
671
- - **Worktree GC scope hardening** (pln#477) — `safeRemoveWorktreeDir` no longer follows symlinks/junctions during cleanup. Closes a class of post-merge wipes that previously destroyed `node_modules` and other neighboring directories on Windows.
672
-
673
- ### v1.0.10–v1.0.12
674
-
675
- - **GitHub Copilot CLI is spawnable** (pln#440) — Copilot CLI 1.0.35+ supports `-p "<prompt>" --allow-all --no-ask-user`; tier promoted to A.
676
- - **Codex spawn on Windows: stdin pipe + 30s handshake TTL** (pln#475) — fixes embedded backticks/`#`/multi-line content getting mis-parsed by `cmd.exe` when a prompt is passed as an inline argument.
677
- - **Brief-ack file handshake** (pln#476) — `.brainclaw/coordination/runtime/ack/<assignmentId>.ack` proves a spawned worker started, decoupling the handshake from MCP availability inside the worker (important for Codex in `--sandbox workspace-write`).
678
- - **`bclaw_loop(intent="open")` orphan-gate** (pln#461) — refuses to open a loop without dispatch unless `allow_orphan: true` is explicit. Use `bclaw_coordinate(intent="review", open_loop: true)` instead.
679
- - **Kilocode** — Tier B integration with native MCP config and live companion (pln#464).
680
-
681
- ### v1.0.0
682
-
683
- - **Canonical grammar promoted to standard tier** — `bclaw_find` / `bclaw_get` / `bclaw_create` / `bclaw_update` / `bclaw_remove` / `bclaw_transition`, plus the entry facades `bclaw_work` and `bclaw_context`, the multi-agent facades `bclaw_coordinate` and `bclaw_dispatch`, and the loop facade `bclaw_loop`. Legacy per-entity tools removed from the discoverable surface (still callable as a migration escape hatch). See `docs/concepts/mcp-governance.md` for tier rules and the deprecation policy.
432
+ Full version history lives in **[CHANGELOG.md](CHANGELOG.md)** (Keep a Changelog
433
+ format, Semantic Versioning). MCP protocol / schema changes and the public surface
434
+ fingerprint are tracked separately in
435
+ **[docs/mcp-schema-changelog.md](docs/mcp-schema-changelog.md)**. For releases
436
+ predating the changelog file, `git log` on `master` is the source of truth each
437
+ release commit follows the `chore(release): bump version to <semver>` convention.
684
438
 
685
439
  ---
686
440
 
Binary file
package/dist/cli.js CHANGED
@@ -782,12 +782,13 @@ program
782
782
  .option('--verify-journal', 'Phase-2 cutover gate (pln#565): rebuild state from the event journal and diff vs live projections; exits non-zero on any drift')
783
783
  .option('--spawn-check', 'Real spawn round-trip per installed agent before dispatch (pln#520 step 2): validates delivery + handshake on this host, exits non-zero on any installed-agent failure')
784
784
  .option('--spawn-check-timeout <ms>', 'Per-agent timeout for --spawn-check (default 15000)', parseInt)
785
+ .option('--hygiene', 'Coordination-hygiene snapshot (pln#602): counts per family, park candidates, serve-count aging stats. Read-only.')
785
786
  .action(async (options) => {
786
787
  if (options.spawnCheck) {
787
788
  await runDoctorSpawnCheck({ cwd: options.cwd, json: options.json, timeoutMs: options.spawnCheckTimeout });
788
789
  return;
789
790
  }
790
- runDoctor({ ...options, afterMigration: options.afterMigration, dispatch: options.dispatch, verifyJournal: options.verifyJournal });
791
+ runDoctor({ ...options, afterMigration: options.afterMigration, dispatch: options.dispatch, verifyJournal: options.verifyJournal, hygiene: options.hygiene });
791
792
  });
792
793
  // --- repair (Phase 4 Sprint 2 Lane C / pln#397) ---
793
794
  program
@@ -1956,7 +1957,14 @@ federationCmd
1956
1957
  const msg = createFederationMessage({
1957
1958
  version: 1,
1958
1959
  from: { project_name: config.project_name, project_path: process.cwd(), agent_name: agent },
1959
- to: { project_name: options.toProject ?? 'broadcast', project_path: '' },
1960
+ to: {
1961
+ project_name: options.toProject ?? 'broadcast',
1962
+ project_path: '',
1963
+ // Wire --to-agent into the message (was declared but dropped, so every
1964
+ // push went out as a broadcast with to_agent NULL — found during the
1965
+ // cross-machine E2E, pln#365). Omitted → undefined → broadcast, as before.
1966
+ ...(options.toAgent ? { agent_name: options.toAgent } : {}),
1967
+ },
1960
1968
  type: options.type,
1961
1969
  payload: { text: message },
1962
1970
  });
@@ -2001,23 +2009,184 @@ federationCmd
2001
2009
  });
2002
2010
  federationCmd
2003
2011
  .command('status')
2004
- .description('Check cloud federation configuration')
2012
+ .description('Diagnose cloud federation: config, health, signing identity, approved agent')
2005
2013
  .action(async () => {
2006
- const { isCloudConfigured } = await import('./core/federation-cloud.js');
2007
- const url = process.env.BRAINCLAW_CLOUD_URL ?? 'https://app.brainclaw.dev';
2008
- console.log(`Cloud URL: ${url}`);
2009
- console.log(`API Key: ${process.env.BRAINCLAW_CLOUD_API_KEY ? '***configured***' : 'NOT SET'}`);
2010
- console.log(`Configured: ${isCloudConfigured() ? 'yes' : 'no'}`);
2011
- if (isCloudConfigured()) {
2012
- try {
2013
- const res = await fetch(`${url}/api/v1/health`);
2014
- const data = await res.json();
2015
- console.log(`Cloud status: ${data.status} (v${data.version})`);
2014
+ const { diagnoseCloudBridge } = await import('./core/federation-cloud.js');
2015
+ const d = await diagnoseCloudBridge();
2016
+ const yn = (b) => (b ? 'yes' : 'no');
2017
+ console.log(`Cloud URL: ${d.apiUrl}`);
2018
+ console.log(`API Key: ${process.env.BRAINCLAW_CLOUD_API_KEY ? '***configured***' : (d.configured ? 'from config' : 'NOT SET')}`);
2019
+ console.log(`Configured: ${yn(d.configured)}`);
2020
+ console.log(`Opted-in: ${yn(d.enabled)}`);
2021
+ console.log(`Project: ${d.projectId ?? '(none)'}`);
2022
+ console.log(`Require signed writes: ${yn(d.requireSigned)}`);
2023
+ if (d.health) {
2024
+ console.log(d.health.ok
2025
+ ? `Cloud status: ${d.health.status} (v${d.health.version})`
2026
+ : `Cloud unreachable: ${d.health.error ?? 'unknown error'}`);
2027
+ }
2028
+ if (d.signing.available) {
2029
+ console.log('\nSigning identity:');
2030
+ console.log(` Agent: ${d.signing.agentName} [${d.signing.cloudAgentId}]`);
2031
+ console.log(` Key present: yes`);
2032
+ console.log(` Fingerprint: ${d.signing.fingerprint.slice(0, 16)}…`);
2033
+ }
2034
+ else {
2035
+ console.log(`\nSigning identity: unavailable — ${d.signing.reason}`);
2036
+ }
2037
+ if (d.approvedAgent) {
2038
+ console.log('\nApproved agent (cloud):');
2039
+ if (d.approvedAgent.found) {
2040
+ console.log(` Status: ${d.approvedAgent.status ?? '(unknown)'}`);
2041
+ console.log(` Trust: ${d.approvedAgent.trustLevel ?? '(unknown)'}`);
2042
+ console.log(` Key match: ${d.approvedAgent.fingerprintMatch ? 'yes ✔' : 'NO ✗ (local key does not match the registered key)'}`);
2043
+ }
2044
+ else {
2045
+ console.log(` Not found${d.approvedAgent.error ? ` — ${d.approvedAgent.error}` : ''}`);
2046
+ }
2047
+ }
2048
+ if (d.requireSigned && !d.signing.available) {
2049
+ console.log('\n⚠ require_signed is set but no signing identity is available — the bridge will refuse to push (fail-closed).');
2050
+ }
2051
+ });
2052
+ federationCmd
2053
+ .command('identity')
2054
+ .description('Show this agent\'s federation signing identity (public key to approve in the cloud UI)')
2055
+ .option('--agent <name>', 'Agent name (defaults to the current agent)')
2056
+ .option('--json', 'Output as JSON')
2057
+ .action(async (options) => {
2058
+ const { resolveOrAutoRegisterAgentIdentity, ensureAgentSigningKey, resolveCurrentAgentName } = await import('./core/agent-registry.js');
2059
+ const agentName = options.agent ?? resolveCurrentAgentName();
2060
+ const { identity } = resolveOrAutoRegisterAgentIdentity({ agentName, cwd: process.cwd() });
2061
+ const key = ensureAgentSigningKey(identity.agent_id);
2062
+ if (options.json) {
2063
+ console.log(JSON.stringify({
2064
+ agent_name: identity.agent_name,
2065
+ local_agent_id: identity.agent_id,
2066
+ fingerprint: key.fingerprint,
2067
+ public_key_pem: key.publicKeyPem,
2068
+ }, null, 2));
2069
+ return;
2070
+ }
2071
+ console.log(`Agent name: ${identity.agent_name}`);
2072
+ console.log(`Local agent id: ${identity.agent_id}`);
2073
+ console.log(`Fingerprint: ${key.fingerprint}`);
2074
+ console.log('\nPublic key — paste into the cloud UI (project → Agents → Register / approve an agent):\n');
2075
+ console.log(key.publicKeyPem.trim());
2076
+ console.log('\nThen, on this machine, configure the bridge with the cloud agent id shown after approval:');
2077
+ console.log(` export BRAINCLAW_AGENT_NAME=${identity.agent_name}`);
2078
+ console.log(' export BRAINCLAW_CLOUD_AGENT_ID=<agt_... returned by the UI>');
2079
+ console.log(' (plus BRAINCLAW_CLOUD_API_KEY, BRAINCLAW_PROJECT_ID) — then run `brainclaw federation status`.');
2080
+ });
2081
+ federationCmd
2082
+ .command('sync')
2083
+ .description('Drain the federation outbox — push signed claim upserts to the cloud')
2084
+ .option('--entity <type>', 'Entity type to sync (increment 1: claim)', 'claim')
2085
+ .option('--limit <n>', 'Max records to push this run')
2086
+ .option('--dry-run', 'Reconcile + list pending records without any network calls')
2087
+ .option('--json', 'Output as JSON')
2088
+ .action(async (options) => {
2089
+ const outbox = await import('./core/federation-outbox.js');
2090
+ const { pushClaimToCloud, isCloudConfigured } = await import('./core/federation-cloud.js');
2091
+ const cwd = process.cwd();
2092
+ const PARK_AFTER = 5;
2093
+ const reconciled = outbox.reconcileOutbox(cwd);
2094
+ let records = outbox.listOutboxRecords(cwd);
2095
+ if (options.limit)
2096
+ records = records.slice(0, parseInt(options.limit, 10));
2097
+ const counts = { synced: 0, superseded: 0, parked: reconciled.parked, dropped: reconciled.dropped, retry: 0 };
2098
+ const lines = [];
2099
+ const emit = (line) => { lines.push(line); if (!options.json)
2100
+ console.log(line); };
2101
+ if (options.dryRun) {
2102
+ for (const r of records)
2103
+ emit(`pending ${r.record.entity_type} ${r.record.entity_id} r${r.record.rev} (${r.record.to_status})`);
2104
+ const out = { dry_run: true, reconciled, pending: records.length, records: lines };
2105
+ if (options.json)
2106
+ console.log(JSON.stringify(out, null, 2));
2107
+ else
2108
+ console.log(`\npending=${records.length} reconciled_dropped=${reconciled.dropped} reconciled_parked=${reconciled.parked}`);
2109
+ return;
2110
+ }
2111
+ if (!isCloudConfigured(cwd)) {
2112
+ console.error('Error: cloud not configured (set BRAINCLAW_CLOUD_API_KEY or cloud_sync). Records left in outbox.');
2113
+ process.exit(3);
2114
+ }
2115
+ let failClosed = false;
2116
+ for (const r of records) {
2117
+ const res = await pushClaimToCloud(r.record.payload, cwd);
2118
+ const tag = `${r.record.entity_id} r${r.record.rev}`;
2119
+ if (res.kind === 'not_configured' || res.kind === 'fail_closed') {
2120
+ failClosed = true;
2121
+ emit(`fail-closed ${tag} (${res.kind}) — not sent`);
2122
+ break; // same config for all remaining records
2016
2123
  }
2017
- catch (e) {
2018
- console.error(`Cloud unreachable: ${e.message}`);
2124
+ if (res.kind === 'network_error') {
2125
+ const attempts = r.record.attempts + 1;
2126
+ if (attempts >= PARK_AFTER) {
2127
+ outbox.parkRecord(r, `network error x${attempts}: ${res.error}`, cwd);
2128
+ counts.parked++;
2129
+ emit(`park ${tag} (network x${attempts}: ${res.error})`);
2130
+ }
2131
+ else {
2132
+ outbox.recordAttempt(r, { http_status: null, error: res.error }, cwd);
2133
+ counts.retry++;
2134
+ emit(`retry ${tag} (network: ${res.error})`);
2135
+ }
2136
+ continue;
2137
+ }
2138
+ const { httpStatus, code } = res;
2139
+ if (httpStatus === 200 || httpStatus === 201) {
2140
+ outbox.archiveToSent(r, { http_status: httpStatus }, cwd);
2141
+ counts.synced++;
2142
+ emit(`pushed ${tag} → ${httpStatus}`);
2143
+ }
2144
+ else if (httpStatus === 409 && (code === 'STALE' || code === 'stale_version')) {
2145
+ outbox.archiveToSent(r, { http_status: httpStatus }, cwd);
2146
+ counts.superseded++;
2147
+ emit(`superseded ${tag} → 409 ${code} (cloud has a newer rev)`);
2148
+ }
2149
+ else if (httpStatus === 409) {
2150
+ outbox.parkRecord(r, `409 ${code ?? 'conflict'}`, cwd);
2151
+ counts.parked++;
2152
+ emit(`PARK ${tag} → 409 ${code ?? 'conflict'} (divergence — inspect)`);
2153
+ }
2154
+ else if (httpStatus === 403) {
2155
+ outbox.parkRecord(r, `403 ${code ?? 'forbidden'}`, cwd);
2156
+ counts.parked++;
2157
+ emit(`PARK ${tag} → 403 ${code ?? 'forbidden'}`);
2158
+ }
2159
+ else if (httpStatus >= 500) {
2160
+ const attempts = r.record.attempts + 1;
2161
+ if (attempts >= PARK_AFTER) {
2162
+ outbox.parkRecord(r, `5xx x${attempts} (last ${httpStatus})`, cwd);
2163
+ counts.parked++;
2164
+ emit(`park ${tag} → ${httpStatus} (x${attempts})`);
2165
+ }
2166
+ else {
2167
+ outbox.recordAttempt(r, { http_status: httpStatus, error: null }, cwd);
2168
+ counts.retry++;
2169
+ emit(`retry ${tag} → ${httpStatus}`);
2170
+ }
2171
+ }
2172
+ else {
2173
+ outbox.parkRecord(r, `${httpStatus} ${code ?? 'client error'}`, cwd);
2174
+ counts.parked++;
2175
+ emit(`PARK ${tag} → ${httpStatus} ${code ?? ''}`.trim());
2019
2176
  }
2020
2177
  }
2178
+ const summary = `synced=${counts.synced} superseded=${counts.superseded} retry=${counts.retry} parked=${counts.parked} dropped=${counts.dropped}`;
2179
+ if (options.json)
2180
+ console.log(JSON.stringify({ ...counts, fail_closed: failClosed, records: lines }, null, 2));
2181
+ else
2182
+ console.log(`\n${summary}`);
2183
+ if (failClosed)
2184
+ process.exit(3);
2185
+ if (counts.parked > 0)
2186
+ process.exit(2);
2187
+ if (counts.retry > 0)
2188
+ process.exit(1);
2189
+ // exit 0
2021
2190
  });
2022
2191
  // --- codev (legacy experimental) ---
2023
2192
  if (isCodevEnabled()) {
@@ -4,6 +4,10 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import * as childProcess from 'node:child_process';
6
6
  import { reconcileAllOpenRuns } from '../core/agentrun-reconciler.js';
7
+ import { loadHygienePolicy } from '../core/hygiene-policy.js';
8
+ import { computeServeStats, loadServeRegistry } from '../core/hint-aging.js';
9
+ import { listAssignments } from '../core/assignments.js';
10
+ import { parkClosedAutoHandoffs } from '../core/gc-semantic.js';
7
11
  import { runSpawnCheck, renderSpawnCheckReport } from '../core/spawn-check.js';
8
12
  import { loadAgentRun } from '../core/agentruns.js';
9
13
  import { listAgentIdentities, listDebrisAgentIdentities, resolveCurrentAgentIdentity } from '../core/agent-registry.js';
@@ -638,7 +642,101 @@ function runJournalVerification(options) {
638
642
  if (drift.length > 0)
639
643
  process.exit(1);
640
644
  }
645
+ function medianAgeDays(items, nowMs) {
646
+ if (items.length === 0)
647
+ return 0;
648
+ const ages = items
649
+ .map((a) => Math.floor((nowMs - new Date(a.created_at).getTime()) / 86_400_000))
650
+ .sort((a, b) => a - b);
651
+ const mid = Math.floor(ages.length / 2);
652
+ return ages.length % 2 === 1 ? ages[mid] : (ages[mid - 1] + ages[mid]) / 2;
653
+ }
654
+ export function runHygieneReport(options = {}) {
655
+ const cwd = options.cwd;
656
+ const policy = loadHygienePolicy(cwd);
657
+ const nowMs = Date.now();
658
+ const allAssignments = listAssignments(cwd);
659
+ const open = allAssignments.filter((a) => a.status === 'offered' || a.status === 'accepted' || a.status === 'started');
660
+ const offered = open.filter((a) => a.status === 'offered');
661
+ const accepted = open.filter((a) => a.status === 'accepted');
662
+ const started = open.filter((a) => a.status === 'started');
663
+ const heartbeatAgeMs = (a) => {
664
+ const anchor = a.last_heartbeat_at ?? a.offered_at ?? a.created_at;
665
+ return nowMs - new Date(anchor).getTime();
666
+ };
667
+ const offered_park_candidates = offered.filter((a) => heartbeatAgeMs(a) > policy.assignment_offered_ttl_ms).length;
668
+ const accepted_park_candidates = accepted.filter((a) => heartbeatAgeMs(a) > policy.assignment_accepted_ttl_ms).length;
669
+ const handoffParkDry = parkClosedAutoHandoffs(cwd ?? process.cwd(), Math.floor(policy.handoff_closed_ttl_ms / 86_400_000), true);
670
+ const registry = loadServeRegistry(cwd);
671
+ const staleStats = computeServeStats(registry.warnings, policy.stale_warning_serve_k);
672
+ const hintsStats = computeServeStats(registry.hints, policy.workflow_hint_serve_k);
673
+ return {
674
+ generated_at: new Date(nowMs).toISOString(),
675
+ disabled: policy.disabled,
676
+ policy,
677
+ families: {
678
+ assignments: {
679
+ total_open: open.length,
680
+ offered: offered.length,
681
+ accepted: accepted.length,
682
+ started: started.length,
683
+ offered_park_candidates,
684
+ accepted_park_candidates,
685
+ median_open_age_days: medianAgeDays(open, nowMs),
686
+ },
687
+ handoffs: {
688
+ closed_park_candidates: handoffParkDry.candidates,
689
+ },
690
+ stale_warnings: {
691
+ total_tracked: staleStats.total,
692
+ over_threshold: staleStats.over_threshold,
693
+ median_count: staleStats.median_count,
694
+ oldest_first_at: staleStats.oldest_first_at,
695
+ },
696
+ workflow_hints: {
697
+ total_tracked: hintsStats.total,
698
+ over_threshold: hintsStats.over_threshold,
699
+ median_count: hintsStats.median_count,
700
+ oldest_first_at: hintsStats.oldest_first_at,
701
+ },
702
+ },
703
+ };
704
+ }
705
+ function renderHygieneReport(report) {
706
+ const lines = [];
707
+ lines.push(`Coordination hygiene — snapshot ${report.generated_at}`);
708
+ if (report.disabled) {
709
+ lines.push(' ✗ policy.disabled=true — hygiene sweep + aging are opted out via config.hygiene.disabled.');
710
+ }
711
+ lines.push('');
712
+ const a = report.families.assignments;
713
+ lines.push(`Assignments (open ${a.total_open}: offered ${a.offered} / accepted ${a.accepted} / started ${a.started}, median age ${a.median_open_age_days}d)`);
714
+ lines.push(` Park candidates: offered=${a.offered_park_candidates}, accepted=${a.accepted_park_candidates} — next sweep at session-start or bclaw_work will converge.`);
715
+ const h = report.families.handoffs;
716
+ lines.push(`Handoffs closed park candidates: ${h.closed_park_candidates} (auto-generated, older than ${Math.floor(report.policy.handoff_closed_ttl_ms / 86_400_000)}d)`);
717
+ const sw = report.families.stale_warnings;
718
+ lines.push(`Stale warnings tracked: ${sw.total_tracked} (over serve-K=${report.policy.stale_warning_serve_k}: ${sw.over_threshold}, median count ${sw.median_count}${sw.oldest_first_at ? `, oldest first-served ${sw.oldest_first_at.slice(0, 10)}` : ''})`);
719
+ const wh = report.families.workflow_hints;
720
+ lines.push(`Workflow hints tracked: ${wh.total_tracked} (over serve-K=${report.policy.workflow_hint_serve_k}: ${wh.over_threshold}, median count ${wh.median_count}${wh.oldest_first_at ? `, oldest first-served ${wh.oldest_first_at.slice(0, 10)}` : ''})`);
721
+ lines.push('');
722
+ lines.push('Read-only: no state was mutated. Session-start and bclaw_work drive the actual sweep/park; the counters age at bclaw_work read paths.');
723
+ return lines.join('\n');
724
+ }
641
725
  export function runDoctor(options = {}) {
726
+ if (options.hygiene) {
727
+ if (!memoryExists(options.cwd)) {
728
+ console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
729
+ process.exit(1);
730
+ }
731
+ const report = runHygieneReport(options);
732
+ if (options.json) {
733
+ console.log(JSON.stringify(report, null, 2));
734
+ }
735
+ else {
736
+ console.log(renderHygieneReport(report));
737
+ }
738
+ return;
739
+ }
642
740
  if (options.verifyJournal) {
643
741
  runJournalVerification(options);
644
742
  return;