brainclaw 1.12.0 → 1.13.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
@@ -66,6 +66,20 @@ Capable agents use the MCP equivalents `bclaw_code_find` / `bclaw_code_brief`, e
66
66
 
67
67
  ---
68
68
 
69
+ ## Measured agent experience
70
+
71
+ The onboarding path a fresh agent takes — init, first `bclaw_work`, first `code_find` — is exercised on every CI run by a reproducible bench (`scripts/bench.mjs`) against a synthetic store calibrated on the shape of a real production project (~200 plans, ~500 handoffs, ~450 claims). Budgets (`bench-budgets.json`) live in the repo alongside the coverage gate; a regression beyond a per-scenario tolerance fails the build. The latest report ships as `dist/facts.json` under `facts.bench` so the site can render current numbers.
72
+
73
+ The bench covers three scenarios:
74
+
75
+ - **Cold onboard** — fresh machine → init → first useful context. Measures the baseline time-to-first-value.
76
+ - **Warm work** — `bclaw_work` consult over a real-shaped store. Tracks the cost of building context when the store is non-empty (the surface `pln#578`/`pln#566` optimise).
77
+ - **First edit** — `code_find` + `code_brief` on the fresh-agent path. Tracks payload compactness (`pln#598`) and match relevance (`pln#601`).
78
+
79
+ Reproduce locally with `npm run bench` (writes `dist/bench-report.json`); check budgets with `npm run bench:check`. The bench runs in-process against synthetic fixtures, so it is deterministic per seed and safe to run on any machine.
80
+
81
+ ---
82
+
69
83
  ## Agent Surfaces
70
84
 
71
85
  brainclaw exposes the same collaboration state through three surfaces, but they do not have the same role in an agent-first workflow.
@@ -417,6 +431,24 @@ npm run test:coverage # with coverage report
417
431
 
418
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)`).
419
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, 2× 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
+
420
452
  ### v1.11.1
421
453
 
422
454
  Agent-identity & session-hook resilience, from a fresh-CLI dogfood on a monorepo:
Binary file
package/dist/cli.js CHANGED
@@ -1179,10 +1179,16 @@ program
1179
1179
  .option('--all', 'Include released claims in list')
1180
1180
  .option('--json', 'Output as JSON for list')
1181
1181
  .option('--plan-status <status>', 'Optional linked plan status when releasing: todo, in_progress, blocked, done, dropped')
1182
+ .option('--coordinator-override', 'Trusted+ only: release a claim owned by another agent')
1182
1183
  .option('--store <target>', 'Target store level: local (default), repo, workspace')
1183
1184
  .option('--local-only', 'Read from local store only for list (skip parent stores in chain)')
1184
1185
  .action((subcommand, args, options) => {
1185
- runClaimResource(subcommand, args, { ...options, planStatus: options.planStatus, localOnly: options.localOnly });
1186
+ runClaimResource(subcommand, args, {
1187
+ ...options,
1188
+ planStatus: options.planStatus,
1189
+ coordinatorOverride: options.coordinatorOverride,
1190
+ localOnly: options.localOnly,
1191
+ });
1186
1192
  });
1187
1193
  // --- assignment ---
1188
1194
  program
@@ -1222,8 +1228,9 @@ program
1222
1228
  .command('release-claim <id>')
1223
1229
  .description('Release a work claim')
1224
1230
  .option('--plan-status <status>', 'Optional linked plan status: todo, in_progress, blocked, done, dropped')
1231
+ .option('--coordinator-override', 'Trusted+ only: release a claim owned by another agent')
1225
1232
  .action((id, options) => {
1226
- runReleaseClaim(id, options);
1233
+ runReleaseClaim(id, { ...options, coordinatorOverride: options.coordinatorOverride });
1227
1234
  });
1228
1235
  // --- release-claims ---
1229
1236
  program
@@ -36,6 +36,7 @@ export function runClaimResource(subcommand, args, options) {
36
36
  }
37
37
  runReleaseClaim(id, {
38
38
  planStatus: options.planStatus,
39
+ coordinatorOverride: options.coordinatorOverride,
39
40
  cwd: options.cwd,
40
41
  });
41
42
  return;
@@ -104,7 +104,7 @@ export function renderRatioBar(ratio, width = 40) {
104
104
  return bar;
105
105
  }
106
106
  export function buildEstimationReport(options = {}) {
107
- const state = loadState(options.cwd);
107
+ const state = options.state ?? loadState(options.cwd);
108
108
  const done = state.plan_items.filter((p) => p.status === 'done' && (!options.agent || p.author === options.agent));
109
109
  const entries = done.map((p) => {
110
110
  // Estimate: prefer the sum of per-step estimates when ALL steps carry one,
@@ -19,7 +19,7 @@ import { listCandidates, listArchivedCandidates, saveCandidate } from '../core/c
19
19
  import { createRuntimeEvent } from '../core/events.js';
20
20
  import { memoryExists } from '../core/io.js';
21
21
  import { loadAssignment, transitionAssignment } from '../core/assignments.js';
22
- import { releaseClaimWithCascade, loadClaim } from '../core/claims.js';
22
+ import { loadClaim, releaseClaimsCascade, logCascadeReleaseResult } from '../core/claims.js';
23
23
  import { getCapabilityProfile, dispatchCanCommit } from '../core/agent-capability.js';
24
24
  import { commitWorktreeOnBehalf, worktreesBaseDir } from '../core/worktree.js';
25
25
  /**
@@ -438,12 +438,16 @@ export function integrateLaneResults(options = {}) {
438
438
  ...entry.files_changed.slice(0, 50).map((f) => ({ type: 'file', ref: f })),
439
439
  ];
440
440
  entry.assignment_completed = forceCompleteAssignment(lane.assignment_id, artifacts, `pln#534 on-behalf integration: ${lane.summary.slice(0, 120)}`, actor, cwd);
441
- try {
442
- const rel = releaseClaimWithCascade(assignment.claim_id, { planStatus: 'done', cwd });
443
- entry.claim_released = rel.claim.status === 'released';
444
- }
445
- catch (err) {
446
- reasons.push(`claim release failed: ${err instanceof Error ? err.message : String(err)}`);
441
+ // trp#928 — use the cascade helper (was releaseClaimWithCascade — same
442
+ // logic for the last-claim rule but the cascade wrapper LOGS per-claim,
443
+ // so a silent ownership failure is observable in the runtime event log
444
+ // rather than only in this in-memory `reasons` string).
445
+ const cascade = releaseClaimsCascade([assignment.claim_id], { cwd, planStatus: 'done' });
446
+ logCascadeReleaseResult({ actor, trigger: 'harvest_integrate', assignment_id: lane.assignment_id, claim_id: assignment.claim_id, cascade, cwd });
447
+ const claimEntry = cascade.entries[0];
448
+ entry.claim_released = claimEntry?.released === true;
449
+ if (claimEntry && !claimEntry.released) {
450
+ reasons.push(`claim release ${claimEntry.reason}${claimEntry.error ? `: ${claimEntry.error}` : ''}`);
447
451
  }
448
452
  }
449
453
  else {
@@ -455,15 +459,15 @@ export function integrateLaneResults(options = {}) {
455
459
  catch (err) {
456
460
  reasons.push(`assignment ${target} transition rejected: ${err instanceof Error ? err.message : String(err)}`);
457
461
  }
458
- try {
459
- const rel = releaseClaimWithCascade(assignment.claim_id, {
460
- planStatus: lane.status === 'blocked' ? 'blocked' : undefined,
461
- cwd,
462
- });
463
- entry.claim_released = rel.claim.status === 'released';
464
- }
465
- catch (err) {
466
- reasons.push(`claim release failed: ${err instanceof Error ? err.message : String(err)}`);
462
+ const cascade = releaseClaimsCascade([assignment.claim_id], {
463
+ cwd,
464
+ planStatus: lane.status === 'blocked' ? 'blocked' : undefined,
465
+ });
466
+ logCascadeReleaseResult({ actor, trigger: 'harvest_integrate', assignment_id: lane.assignment_id, claim_id: assignment.claim_id, cascade, cwd });
467
+ const claimEntry = cascade.entries[0];
468
+ entry.claim_released = claimEntry?.released === true;
469
+ if (claimEntry && !claimEntry.released) {
470
+ reasons.push(`claim release ${claimEntry.reason}${claimEntry.error ? `: ${claimEntry.error}` : ''}`);
467
471
  }
468
472
  }
469
473
  // Durable trace of the integration.
@@ -615,12 +619,16 @@ export function harvestOrphaned(options) {
615
619
  ...report.files_changed.slice(0, 50).map((f) => ({ type: 'file', ref: f })),
616
620
  ];
617
621
  report.assignment_completed = forceCompleteAssignment(options.assignmentId, artifacts, 'pln#554 harvest --orphaned: worker died before delivering; work recovered from worktree', actor, cwd);
618
- try {
619
- const rel = releaseClaimWithCascade(assignment.claim_id, { planStatus: 'done', cwd });
620
- report.claim_released = rel.claim.status === 'released';
621
- }
622
- catch (err) {
623
- report.errors.push(`claim release failed: ${err instanceof Error ? err.message : String(err)}`);
622
+ // trp#928 — log per-claim via releaseClaimsCascade instead of the raw
623
+ // releaseClaimWithCascade so an ownership_denied outcome is visible in the
624
+ // runtime event log (previously trapped into report.errors only, which
625
+ // dies with the CLI invocation).
626
+ const cascade = releaseClaimsCascade([assignment.claim_id], { cwd, planStatus: 'done' });
627
+ logCascadeReleaseResult({ actor, trigger: 'harvest_integrate', assignment_id: options.assignmentId, claim_id: assignment.claim_id, cascade, cwd });
628
+ const claimEntry = cascade.entries[0];
629
+ report.claim_released = claimEntry?.released === true;
630
+ if (claimEntry && !claimEntry.released) {
631
+ report.errors.push(`claim release ${claimEntry.reason}${claimEntry.error ? `: ${claimEntry.error}` : ''}`);
624
632
  }
625
633
  }
626
634
  else {