brainclaw 1.16.0 → 1.18.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 (85) hide show
  1. package/README.md +22 -8
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-review.js +2 -2
  4. package/dist/commands/code-map.js +4 -1
  5. package/dist/commands/codev.js +61 -30
  6. package/dist/commands/doctor.js +14 -1
  7. package/dist/commands/harvest.js +241 -25
  8. package/dist/commands/inbox.js +10 -4
  9. package/dist/commands/loop.js +2 -2
  10. package/dist/commands/loops-handlers.js +82 -1
  11. package/dist/commands/mcp-catalog.js +12 -4
  12. package/dist/commands/mcp-read-handlers.js +90 -7
  13. package/dist/commands/mcp-schemas.generated.js +3 -0
  14. package/dist/commands/mcp-write-coordination.js +159 -40
  15. package/dist/commands/mcp.js +11 -2
  16. package/dist/core/agent-capability.js +7 -2
  17. package/dist/core/agent-files.js +53 -2
  18. package/dist/core/agent-integrations.js +1 -0
  19. package/dist/core/agentrun-reconciler.js +171 -7
  20. package/dist/core/agentruns.js +6 -1
  21. package/dist/core/code-map/aggregate.js +473 -0
  22. package/dist/core/code-map/backend.js +36 -10
  23. package/dist/core/code-map/freshness.js +36 -1
  24. package/dist/core/code-map/lang/c/imports.scm +12 -0
  25. package/dist/core/code-map/lang/c/index.js +150 -0
  26. package/dist/core/code-map/lang/c/tags.scm +68 -0
  27. package/dist/core/code-map/lang/cpp/imports.scm +14 -0
  28. package/dist/core/code-map/lang/cpp/index.js +149 -0
  29. package/dist/core/code-map/lang/cpp/tags.scm +87 -0
  30. package/dist/core/code-map/lang/csharp/imports.scm +20 -0
  31. package/dist/core/code-map/lang/csharp/index.js +224 -0
  32. package/dist/core/code-map/lang/csharp/tags.scm +63 -0
  33. package/dist/core/code-map/lang/go/imports.scm +13 -0
  34. package/dist/core/code-map/lang/go/index.js +139 -0
  35. package/dist/core/code-map/lang/go/tags.scm +36 -0
  36. package/dist/core/code-map/lang/providers.js +12 -1
  37. package/dist/core/code-map/lang/ruby/imports.scm +24 -0
  38. package/dist/core/code-map/lang/ruby/index.js +198 -0
  39. package/dist/core/code-map/lang/ruby/tags.scm +49 -0
  40. package/dist/core/code-map/lang/rust/imports.scm +44 -0
  41. package/dist/core/code-map/lang/rust/index.js +136 -0
  42. package/dist/core/code-map/lang/rust/tags.scm +47 -0
  43. package/dist/core/code-map/query.js +229 -80
  44. package/dist/core/code-map/types.js +18 -0
  45. package/dist/core/code-map/work-section.js +8 -7
  46. package/dist/core/codev-responses.js +16 -0
  47. package/dist/core/dispatcher.js +209 -29
  48. package/dist/core/execution-adapters.js +29 -3
  49. package/dist/core/ideation-loop-close.js +124 -0
  50. package/dist/core/loops/artifact-resolver.js +197 -0
  51. package/dist/core/loops/attempt-reservation.js +576 -0
  52. package/dist/core/loops/commit-intent.js +494 -0
  53. package/dist/core/loops/facade-schema.js +48 -0
  54. package/dist/core/loops/impl-bind.js +144 -0
  55. package/dist/core/loops/index.js +1 -1
  56. package/dist/core/loops/iteration-engine.js +29 -0
  57. package/dist/core/loops/lock.js +14 -0
  58. package/dist/core/loops/project-resolution.js +157 -0
  59. package/dist/core/loops/reconcile-turn.js +369 -0
  60. package/dist/core/loops/result-reducers.js +88 -0
  61. package/dist/core/loops/store.js +46 -7
  62. package/dist/core/loops/types.js +139 -11
  63. package/dist/core/loops/verbs.js +9 -3
  64. package/dist/core/loops/verify-command.js +209 -0
  65. package/dist/core/messaging.js +58 -5
  66. package/dist/core/review-loop-close.js +106 -34
  67. package/dist/core/review-loop-turn-dispatch.js +445 -0
  68. package/dist/core/runtime-signals.js +68 -0
  69. package/dist/core/schema.js +34 -0
  70. package/dist/core/worktree.js +240 -22
  71. package/dist/facts.js +10 -10
  72. package/dist/facts.json +9 -9
  73. package/dist/wasm/tree-sitter-c.wasm +0 -0
  74. package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  75. package/dist/wasm/tree-sitter-cpp.wasm +0 -0
  76. package/dist/wasm/tree-sitter-go.wasm +0 -0
  77. package/dist/wasm/tree-sitter-ruby.wasm +0 -0
  78. package/dist/wasm/tree-sitter-rust.wasm +0 -0
  79. package/docs/cli.md +1 -1
  80. package/docs/code-map.md +22 -6
  81. package/docs/concepts/loop-engine.md +28 -2
  82. package/docs/concepts/observer-protocol.md +22 -0
  83. package/docs/integrations/codex.md +19 -3
  84. package/docs/mcp-schema-changelog.md +43 -1
  85. package/package.json +1 -1
@@ -114,6 +114,74 @@ export function readHeartbeat(root, assignmentId, worktreePath) {
114
114
  return projectInfo;
115
115
  return (worktreeInfo.mtimeMs ?? 0) > (projectInfo.mtimeMs ?? 0) ? worktreeInfo : projectInfo;
116
116
  }
117
+ /**
118
+ * Write a turn-keyed completion/failed sentinel body. Used when brainclaw itself
119
+ * (wrapper/reconcile) writes the sentinel; the shell `&& completed` fallback
120
+ * still produces a legacy presence-only marker, which stays a valid life-sign
121
+ * via signalExists but is NOT accepted as turn-owned evidence (PR2b-c).
122
+ */
123
+ export function writeCompletionSignal(root, assignmentId, body) {
124
+ const p = getRuntimeSignalPath(root, assignmentId, body.status);
125
+ fs.mkdirSync(path.dirname(p), { recursive: true });
126
+ fs.writeFileSync(p, JSON.stringify(body), 'utf-8');
127
+ }
128
+ /** Parse ONE turn-keyed sentinel body, or undefined if absent / legacy
129
+ * presence-only / non-JSON / missing correlation keys. Never throws. */
130
+ function readOneCompletionSignal(root, assignmentId, status) {
131
+ let raw;
132
+ try {
133
+ raw = fs.readFileSync(getRuntimeSignalPath(root, assignmentId, status), 'utf-8').trim();
134
+ }
135
+ catch {
136
+ return undefined; // sentinel absent
137
+ }
138
+ if (!raw)
139
+ return undefined; // legacy presence-only (empty) marker
140
+ try {
141
+ const parsed = JSON.parse(raw);
142
+ if (typeof parsed.turn_id === 'string' &&
143
+ typeof parsed.run_id === 'string' &&
144
+ typeof parsed.nonce === 'string' &&
145
+ (parsed.status === 'completed' || parsed.status === 'failed')) {
146
+ return {
147
+ turn_id: parsed.turn_id,
148
+ run_id: parsed.run_id,
149
+ nonce: parsed.nonce,
150
+ status: parsed.status,
151
+ at: typeof parsed.at === 'string' ? parsed.at : '',
152
+ };
153
+ }
154
+ }
155
+ catch { /* non-JSON legacy body */ }
156
+ return undefined;
157
+ }
158
+ /**
159
+ * Read BOTH turn-keyed completion sentinels for an attempt. This is the
160
+ * authoritative reader for the read-strict acceptance path: it surfaces a
161
+ * `completed`+`failed` contradiction so the caller can raise a conflict event
162
+ * and WITHHOLD an irreversible auto-stop (spec §13 R4), rather than silently
163
+ * collapsing to one. Legacy presence-only markers read as absent here.
164
+ */
165
+ export function readCompletionSignals(root, assignmentId) {
166
+ const out = {};
167
+ const completed = readOneCompletionSignal(root, assignmentId, 'completed');
168
+ const failed = readOneCompletionSignal(root, assignmentId, 'failed');
169
+ if (completed)
170
+ out.completed = completed;
171
+ if (failed)
172
+ out.failed = failed;
173
+ return out;
174
+ }
175
+ /**
176
+ * Convenience single-body reader (`completed` preferred over `failed`). Returns
177
+ * undefined for absent / legacy presence-only / non-JSON / missing-keys.
178
+ * CALLERS THAT ACT IRREVERSIBLY must use {@link readCompletionSignals} instead
179
+ * so a completed+failed contradiction is not hidden (spec §13 R4).
180
+ */
181
+ export function readCompletionSignal(root, assignmentId) {
182
+ const both = readCompletionSignals(root, assignmentId);
183
+ return both.completed ?? both.failed;
184
+ }
117
185
  /**
118
186
  * can_c39f0961 — CP850 high-byte table (0x80–0xFF). Windows-native console
119
187
  * tools write redirected stdout/stderr in the OEM codepage (cp850 on western
@@ -479,6 +479,13 @@ export const InboxMessageSchema = z.object({
479
479
  read_at: z.string().optional(),
480
480
  /** When the message was acknowledged */
481
481
  ack_at: z.string().optional(),
482
+ /** True when the body was truncated at WRITE time because it exceeded the
483
+ * inline size cap (pln#627 Phase B). Unlike read-time previews, the omitted
484
+ * tail is NOT stored inline — the full artifact belongs in a dedicated store
485
+ * (e.g. ideation responses), with the message carrying only a pointer. */
486
+ truncated_at_write: z.boolean().optional(),
487
+ /** Original body length in characters before write-time truncation. */
488
+ original_text_length: z.number().int().nonnegative().optional(),
482
489
  created_at: z.string(),
483
490
  updated_at: z.string(),
484
491
  author: z.string(),
@@ -956,6 +963,9 @@ export const RuntimeEventTypeSchema = z.enum([
956
963
  'candidate_harvested',
957
964
  'lane_result_harvested',
958
965
  'lane_integrated',
966
+ // pln#521 P4 — a turn-owned loop artifact was harvested + integrated into the loop
967
+ // by reconcileTurn (observability for the harvest path).
968
+ 'loop_artifact_harvested',
959
969
  ]);
960
970
  /**
961
971
  * pln#526 — LANE-RESULT convention. A dispatched worker writes a single
@@ -966,6 +976,16 @@ export const RuntimeEventTypeSchema = z.enum([
966
976
  */
967
977
  export const LaneResultSchema = z.object({
968
978
  assignment_id: z.string(),
979
+ /**
980
+ * pln#630 PR2b-a (§13 R2/R3) — turn-attempt correlation keys. Optional for
981
+ * backward compat (legacy lanes are assignment-keyed only); a loop-dispatched
982
+ * lane echoes all three so the read-strict acceptance path can prove WHICH
983
+ * attempt+generation produced this result. `nonce` == the consumed launch
984
+ * token (the epoch-unique generation id), NOT a turn_id-bound value.
985
+ */
986
+ turn_id: z.string().optional(),
987
+ run_id: z.string().optional(),
988
+ nonce: z.string().optional(),
969
989
  status: z.enum(['completed', 'blocked', 'failed']),
970
990
  summary: z.string(),
971
991
  /** Paths or refs the worker produced (commits, files, docs). */
@@ -999,6 +1019,10 @@ export const RuntimeEventSchema = z.object({
999
1019
  tags: TagsWithDefaultSchema,
1000
1020
  assignment_id: z.string().optional(),
1001
1021
  run_id: z.string().optional(),
1022
+ // pln#630 PR2b-a (§13 R2/R3) — turn-attempt correlation on runtime signals.
1023
+ // `run_id` already present above; `nonce` == launch-generation token.
1024
+ turn_id: z.string().optional(),
1025
+ nonce: z.string().optional(),
1002
1026
  claim_id: z.string().optional(),
1003
1027
  message_id: z.string().optional(),
1004
1028
  plan_id: z.string().optional(),
@@ -1454,6 +1478,16 @@ export const ConfigSchema = z.object({
1454
1478
  worktree: z.object({
1455
1479
  shared_paths: z.array(z.string()).default([]),
1456
1480
  exclude_shared: z.array(z.string()).default([]),
1481
+ /**
1482
+ * How a dispatched worktree gets its JS dependencies (`node_modules`).
1483
+ * trp_37b05a15 — the default `link` (out-of-root junction) is rejected by
1484
+ * `next dev` / Turbopack. Set `install` (real per-worktree install, native
1485
+ * package manager) or `copy` (recursive copy from the main tree) for a
1486
+ * Turbopack-compatible in-root `node_modules`; `none` provisions no deps.
1487
+ * Env `BRAINCLAW_WORKTREE_DEPS_MODE` overrides this; `BRAINCLAW_NO_LINK_DEPS=1`
1488
+ * still forces `none`.
1489
+ */
1490
+ deps_mode: z.enum(['link', 'install', 'copy', 'none']).optional(),
1457
1491
  }).optional(),
1458
1492
  // Event-log store (pln#543). Absent ⇒ off — fresh and existing stores keep
1459
1493
  // today's behavior; the journal only activates when explicitly set here (or
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import yaml from 'yaml';
7
7
  import { logger } from './logger.js';
8
+ import { loadConfig } from './config.js';
8
9
  import { parsePorcelainZ, isSystemDirtyPath } from './dirty-scope.js';
9
10
  /** Normalizes a path for use in git CLI arguments (forward slashes on Windows). */
10
11
  function gitPath(p) {
@@ -93,6 +94,67 @@ export function detectStackSharedPaths(projectRoot) {
93
94
  }
94
95
  return [...result];
95
96
  }
97
+ const WORKTREE_DEPS_MODES = ['link', 'install', 'copy', 'none'];
98
+ /**
99
+ * Resolves the JS dependency provisioning mode for a worktree.
100
+ *
101
+ * Precedence (first match wins):
102
+ * 1. env `BRAINCLAW_WORKTREE_DEPS_MODE` (link|install|copy|none)
103
+ * 2. env `BRAINCLAW_NO_LINK_DEPS=1` → `none` (backward compat)
104
+ * 3. config `worktree.deps_mode` in `.brainclaw/config.yaml`
105
+ * 4. `link` (default — unchanged behavior)
106
+ *
107
+ * An unrecognized env value is ignored (falls through) with a warning, so a
108
+ * typo never silently changes provisioning.
109
+ */
110
+ export function resolveWorktreeDepsMode(projectRoot) {
111
+ const envMode = process.env.BRAINCLAW_WORKTREE_DEPS_MODE?.trim().toLowerCase();
112
+ if (envMode) {
113
+ if (WORKTREE_DEPS_MODES.includes(envMode)) {
114
+ return envMode;
115
+ }
116
+ logger.warn(`[worktree] Ignoring invalid BRAINCLAW_WORKTREE_DEPS_MODE='${envMode}' `
117
+ + `(expected one of ${WORKTREE_DEPS_MODES.join('|')}).`);
118
+ }
119
+ if (process.env.BRAINCLAW_NO_LINK_DEPS === '1')
120
+ return 'none';
121
+ try {
122
+ const configured = loadConfig(projectRoot).worktree?.deps_mode;
123
+ if (configured && WORKTREE_DEPS_MODES.includes(configured)) {
124
+ return configured;
125
+ }
126
+ }
127
+ catch { /* no / invalid config — fall through to default */ }
128
+ return 'link';
129
+ }
130
+ /**
131
+ * Detects the JS package manager for a project from its lockfile, falling back
132
+ * to the `packageManager` field of package.json, then to `npm`. Lockfile wins
133
+ * because it reflects what actually produced the main tree's `node_modules`.
134
+ */
135
+ export function detectPackageManager(projectRoot) {
136
+ const lockfiles = [
137
+ ['pnpm-lock.yaml', 'pnpm'],
138
+ ['yarn.lock', 'yarn'],
139
+ ['bun.lockb', 'bun'],
140
+ ['bun.lock', 'bun'],
141
+ ['package-lock.json', 'npm'],
142
+ ['npm-shrinkwrap.json', 'npm'],
143
+ ];
144
+ for (const [file, pm] of lockfiles) {
145
+ if (fs.existsSync(path.join(projectRoot, file)))
146
+ return pm;
147
+ }
148
+ try {
149
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8'));
150
+ const declared = pkg.packageManager?.split('@')[0]?.trim();
151
+ if (declared === 'pnpm' || declared === 'yarn' || declared === 'bun' || declared === 'npm') {
152
+ return declared;
153
+ }
154
+ }
155
+ catch { /* no / invalid package.json — default below */ }
156
+ return 'npm';
157
+ }
96
158
  /**
97
159
  * pln#523 — read declared monorepo workspace globs from npm/yarn/bun
98
160
  * `workspaces` (package.json) and pnpm-workspace.yaml. Returns the raw
@@ -555,6 +617,106 @@ export function projectUsesNextjs(projectRoot) {
555
617
  return false;
556
618
  }
557
619
  }
620
+ /**
621
+ * Timeout for a per-worktree package-manager install (`deps_mode=install`).
622
+ * Defaults to 10 minutes; override with BRAINCLAW_WORKTREE_INSTALL_TIMEOUT_MS.
623
+ */
624
+ export function resolveWorktreeInstallTimeoutMs() {
625
+ const raw = process.env.BRAINCLAW_WORKTREE_INSTALL_TIMEOUT_MS;
626
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
627
+ return Number.isFinite(n) && n > 0 ? n : 600_000;
628
+ }
629
+ /**
630
+ * Provisions a worktree's JS dependencies for `install` / `copy` deps modes,
631
+ * yielding a real in-root `node_modules` that `next dev` / Turbopack accepts
632
+ * (unlike the out-of-root junction of `link` mode; trp_37b05a15).
633
+ *
634
+ * Best-effort: any failure is recorded as a warning (the worker can still
635
+ * install by hand) and NEVER thrown — worktree creation must not fail over
636
+ * dependency provisioning. Returns human-readable warnings (empty on success).
637
+ *
638
+ * - `install` runs ONE package-manager install at the worktree root, which
639
+ * natively populates monorepo workspace `node_modules` too — so it ignores
640
+ * `nodeModulesRelPaths`. No-op when the project has no `package.json`.
641
+ * - `copy` recursively mirrors each existing `node_modules` dir from the main
642
+ * tree (symlinks copied verbatim so pnpm's relative link farm stays valid).
643
+ */
644
+ export function provisionWorktreeDeps(mode, mainWorktreePath, targetPath, nodeModulesRelPaths) {
645
+ const warnings = [];
646
+ if (mode === 'install') {
647
+ if (!fs.existsSync(path.join(targetPath, 'package.json')))
648
+ return warnings;
649
+ const pm = detectPackageManager(mainWorktreePath);
650
+ const timeoutMs = resolveWorktreeInstallTimeoutMs();
651
+ // Windows: npm/pnpm/yarn/bun are `.cmd` shims, only found via the shell — so
652
+ // pass ONE static command string (pm is validated; 'install' is literal → no
653
+ // injection) and NO args array (avoids DEP0190). Unix: the binaries are on
654
+ // PATH, so spawn directly with an args array and no shell.
655
+ const result = process.platform === 'win32'
656
+ ? spawnSync(`${pm} install`, { cwd: targetPath, encoding: 'utf-8', timeout: timeoutMs, shell: true })
657
+ : spawnSync(pm, ['install'], { cwd: targetPath, encoding: 'utf-8', timeout: timeoutMs });
658
+ if (result.error?.code === 'ETIMEDOUT') {
659
+ const msg = `deps_mode=install: '${pm} install' timed out after ${timeoutMs}ms and was killed `
660
+ + `(raise BRAINCLAW_WORKTREE_INSTALL_TIMEOUT_MS). Run '${pm} install' in the worktree manually.`;
661
+ warnings.push(msg);
662
+ logger.warn(`[worktree] ${msg}`);
663
+ }
664
+ else if (result.error) {
665
+ const msg = `deps_mode=install: could not run '${pm} install' (${result.error.message}). `
666
+ + `Is ${pm} on PATH? Run '${pm} install' in the worktree manually.`;
667
+ warnings.push(msg);
668
+ logger.warn(`[worktree] ${msg}`);
669
+ }
670
+ else if (result.status !== 0) {
671
+ const tail = (result.stderr || result.stdout || '').trim().split(/\r?\n/).filter(Boolean).slice(-3).join(' | ');
672
+ const msg = `deps_mode=install: '${pm} install' exited ${result.status ?? '?'}${tail ? ` — ${tail}` : ''}. `
673
+ + `Run '${pm} install' in the worktree manually.`;
674
+ warnings.push(msg);
675
+ logger.warn(`[worktree] ${msg}`);
676
+ }
677
+ return warnings;
678
+ }
679
+ // copy
680
+ const copyable = nodeModulesRelPaths.filter((rel) => fs.existsSync(path.join(mainWorktreePath, rel)));
681
+ if (copyable.length === 0) {
682
+ if (fs.existsSync(path.join(targetPath, 'package.json'))) {
683
+ const pm = detectPackageManager(mainWorktreePath);
684
+ const msg = `deps_mode=copy: no node_modules found in the main tree to copy — `
685
+ + `run '${pm} install' in the worktree.`;
686
+ warnings.push(msg);
687
+ logger.warn(`[worktree] ${msg}`);
688
+ }
689
+ return warnings;
690
+ }
691
+ for (const rel of copyable) {
692
+ const src = path.join(mainWorktreePath, rel);
693
+ const dest = path.join(targetPath, rel);
694
+ if (fs.existsSync(dest))
695
+ continue;
696
+ try {
697
+ const parentDir = path.dirname(dest);
698
+ if (parentDir !== targetPath)
699
+ fs.mkdirSync(parentDir, { recursive: true });
700
+ // Codex review P1: if the SOURCE node_modules is itself a symlink/junction
701
+ // (e.g. a main tree that is itself a linked worktree, or a user-linked
702
+ // node_modules), a verbatim copy would reproduce that out-of-root link and
703
+ // Turbopack would still reject it — defeating copy mode. Dereference the
704
+ // TOP-LEVEL entry to its real directory before copying, then copy with
705
+ // verbatimSymlinks so the tree's INTERNAL relative links (pnpm's farm)
706
+ // stay intact. A real dir source copies straight through.
707
+ const srcReal = fs.lstatSync(src).isSymbolicLink() ? fs.realpathSync(src) : src;
708
+ fs.cpSync(srcReal, dest, { recursive: true, verbatimSymlinks: true });
709
+ }
710
+ catch (err) {
711
+ const reason = err instanceof Error ? err.message : String(err);
712
+ const msg = `deps_mode=copy: failed to copy '${rel}' into worktree (${reason}). `
713
+ + `Run the package manager's install in the worktree manually.`;
714
+ warnings.push(msg);
715
+ logger.warn(`[worktree] ${msg}`);
716
+ }
717
+ }
718
+ return warnings;
719
+ }
558
720
  export function createWorktree(mainWorktreePath, branchName, options = {}) {
559
721
  // pln#614: resolve the true git toplevel first, so an in-tree project (project
560
722
  // dir ≠ git root) creates its worktree from the real repo root — `git worktree
@@ -669,35 +831,59 @@ export function createWorktree(mainWorktreePath, branchName, options = {}) {
669
831
  // `dist` intentionally excluded — build outputs must be per-worktree
670
832
  // (EBUSY during clean:dist when MCP/extension holds a handle on junction target).
671
833
  // pln#523: also link per-package node_modules for JS/TS monorepos so workers
672
- // can build/typecheck sub-packages, not just the root. Set
673
- // BRAINCLAW_NO_LINK_DEPS=1 to disable auto dependency linking (e.g. when the
674
- // worktree lives on a different volume and central validation is preferred);
675
- // explicit options.sharedPaths are still honored.
676
- const linkDepsDisabled = process.env.BRAINCLAW_NO_LINK_DEPS === '1';
677
- const detected = linkDepsDisabled
834
+ // can build/typecheck sub-packages, not just the root.
835
+ //
836
+ // trp_37b05a15: the JS dependency provisioning mode (link | install | copy |
837
+ // none) is opt-in via BRAINCLAW_WORKTREE_DEPS_MODE / config worktree.deps_mode
838
+ // (BRAINCLAW_NO_LINK_DEPS=1 still maps to `none`). `link` (default) junctions
839
+ // node_modules from the main tree — an out-of-root symlink `next dev` rejects;
840
+ // `install`/`copy` provision a real in-root node_modules (Turbopack-ok);
841
+ // `none` provisions no deps (central validation). Explicit options.sharedPaths
842
+ // are always honored.
843
+ const isNodeModulesPath = (p) => p === 'node_modules' || p.endsWith('/node_modules');
844
+ const depsMode = resolveWorktreeDepsMode(mainWorktreePath);
845
+ const detected = depsMode === 'none'
678
846
  ? []
679
847
  : [...detectStackSharedPaths(mainWorktreePath), ...detectWorkspaceNodeModules(mainWorktreePath)];
680
848
  const extra = options.sharedPaths ?? [];
681
849
  const excluded = new Set(options.excludeShared ?? []);
682
- const sharedPaths = [...new Set([...detected, ...extra])].filter((p) => !excluded.has(p));
850
+ const requested = [...new Set([...detected, ...extra])].filter((p) => !excluded.has(p));
851
+ // In install/copy mode, node_modules becomes a REAL in-root directory instead
852
+ // of an out-of-root junction — so it is excluded from the symlink pass and
853
+ // provisioned separately. Other stack dirs (venv, vendor, …) still link.
854
+ const provisionDeps = depsMode === 'install' || depsMode === 'copy';
855
+ const nodeModulesPaths = requested.filter(isNodeModulesPath);
856
+ const sharedPaths = provisionDeps ? requested.filter((p) => !isNodeModulesPath(p)) : requested;
683
857
  for (const entry of sharedPaths) {
684
858
  trySymlinkSharedPath(entry);
685
859
  }
686
- // trp_37b05a15 (field report, Next.js 16 / Turbopack) — the node_modules link
687
- // brainclaw provisions is an out-of-worktree-root symlink to the main repo.
688
- // tsc / vitest / build follow it fine, but `next dev` (Turbopack) PANICS on a
689
- // node_modules link that points outside the worktree root. Surface a warning
690
- // (not a failure — the link is still correct for build/typecheck) so a worker
691
- // or operator doing dev-server work knows the workaround up front. A full
692
- // Turbopack-compatible per-worktree dependency mode is a planned follow-up.
693
- const linkedNodeModules = sharedPaths.some((p) => p === 'node_modules' || p.endsWith('/node_modules'));
694
- if (linkedNodeModules && projectUsesNextjs(mainWorktreePath)) {
695
- const msg = 'Next.js detected: node_modules is linked as an out-of-worktree-root symlink, which '
696
- + '`next dev` / Turbopack rejects (it requires node_modules under the worktree root). '
697
- + 'tsc / vitest / build are unaffected. For dev-server work in this worktree, run '
698
- + '`npm install` here (optionally with BRAINCLAW_NO_LINK_DEPS=1), or smoke-test on the merged branch.';
699
- symlinkWarnings.push(msg);
700
- logger.warn(`[worktree] ${msg}`);
860
+ // Codex review P1: track whether in-root provisioning actually succeeded, so
861
+ // the dispatch brief can tell the worker the truth. A failed install/copy is
862
+ // best-effort (non-fatal) but the worker must then install itself — the brief
863
+ // must NOT claim "node_modules is real, do not reinstall" over a failure.
864
+ let depsProvisioned;
865
+ if (provisionDeps) {
866
+ const provisionWarnings = provisionWorktreeDeps(depsMode, mainWorktreePath, targetPath, nodeModulesPaths);
867
+ symlinkWarnings.push(...provisionWarnings);
868
+ depsProvisioned = provisionWarnings.length === 0;
869
+ }
870
+ else if (depsMode === 'link') {
871
+ // trp_37b05a15 (field report, Next.js 16 / Turbopack) — the node_modules link
872
+ // brainclaw provisions is an out-of-worktree-root symlink to the main repo.
873
+ // tsc / vitest / build follow it fine, but `next dev` (Turbopack) PANICS on a
874
+ // node_modules link that points outside the worktree root. Surface a warning
875
+ // (not a failure — the link is still correct for build/typecheck) so a worker
876
+ // or operator doing dev-server work knows the workaround up front.
877
+ const linkedNodeModules = sharedPaths.some(isNodeModulesPath);
878
+ if (linkedNodeModules && projectUsesNextjs(mainWorktreePath)) {
879
+ const msg = 'Next.js detected: node_modules is linked as an out-of-worktree-root symlink, which '
880
+ + '`next dev` / Turbopack rejects (it requires node_modules under the worktree root). '
881
+ + 'tsc / vitest / build are unaffected. For dev-server work, set deps_mode=install '
882
+ + '(config worktree.deps_mode or BRAINCLAW_WORKTREE_DEPS_MODE=install), run `npm install` '
883
+ + 'here, or smoke-test on the merged branch.';
884
+ symlinkWarnings.push(msg);
885
+ logger.warn(`[worktree] ${msg}`);
886
+ }
701
887
  }
702
888
  // NOTE: .brainclaw/ is intentionally NOT symlinked.
703
889
  // Symlinking .brainclaw/ causes hooks and session_start to trigger on the
@@ -737,6 +923,14 @@ export function createWorktree(mainWorktreePath, branchName, options = {}) {
737
923
  ...(baseRefSha ? { base_ref_sha: baseRefSha } : {}),
738
924
  reset_existing_branch: options.resetExistingBranch === true,
739
925
  git_advice: 'git add ONLY specific files, NEVER git add -A.',
926
+ // trp_37b05a15: how JS deps were provisioned (link junction / real install /
927
+ // copy / none) — non-default modes are recorded so a worker/supervisor knows
928
+ // whether node_modules is an out-of-root link (dev-server caveat) or in-root.
929
+ // `deps_provisioned` (install/copy only) records whether the in-root
930
+ // provisioning actually succeeded — false means best-effort failed and the
931
+ // worker must install itself (Codex review P1).
932
+ ...(depsMode !== 'link' ? { deps_mode: depsMode } : {}),
933
+ ...(depsProvisioned !== undefined ? { deps_provisioned: depsProvisioned } : {}),
740
934
  // pln#523: surface any shared-path link failures (e.g. node_modules junction
741
935
  // that could not be created) so the worker / supervisor can see why a build
742
936
  // might fail, instead of an invisible degradation.
@@ -1166,6 +1360,30 @@ export function isBranchMergedByContent(mainWorktreePath, branchName, baseRef =
1166
1360
  }
1167
1361
  return true;
1168
1362
  }
1363
+ /**
1364
+ * True when a LOCAL git branch of this exact name exists (pln#529). Lets the
1365
+ * gated-sequence base selector distinguish "predecessor branch gone (merged +
1366
+ * cleaned → code is on HEAD)" from "branch present but not yet integrated →
1367
+ * fork the dependent lane from it". Returns false on any git failure.
1368
+ */
1369
+ export function localBranchExists(mainWorktreePath, branchName) {
1370
+ return probeLocalBranch(mainWorktreePath, branchName) === 'present';
1371
+ }
1372
+ export function probeLocalBranch(mainWorktreePath, branchName) {
1373
+ const r = runGit(['rev-parse', '--verify', '--quiet', `refs/heads/${branchName}`], mainWorktreePath);
1374
+ if (r.ok)
1375
+ return 'present';
1376
+ return r.stderr.trim() === '' ? 'absent' : 'unknown';
1377
+ }
1378
+ /**
1379
+ * True when `cwd` is inside a git work tree. pln#529 uses this to distinguish a
1380
+ * NON-git project (where branch/worktree propagation is inapplicable — fall back
1381
+ * to the legacy HEAD base) from a git repo whose branch probe transiently failed
1382
+ * (which must fail SAFE, not silently assume HEAD).
1383
+ */
1384
+ export function isGitRepo(cwd) {
1385
+ return runGit(['rev-parse', '--is-inside-work-tree'], cwd).ok;
1386
+ }
1169
1387
  /**
1170
1388
  * Removes worktrees whose branch has been fully merged into the current branch
1171
1389
  * (typically master/main after a merge). Also removes brainclaw-managed
package/dist/facts.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
2
- // Source: brainclaw v1.16.0 on 2026-07-18T18:21:20.312Z
2
+ // Source: brainclaw v1.18.0 on 2026-07-31T14:51:44.969Z
3
3
  export const FACTS = {
4
- "version": "1.16.0",
5
- "generated_at": "2026-07-18T18:21:20.312Z",
4
+ "version": "1.18.0",
5
+ "generated_at": "2026-07-31T14:51:44.969Z",
6
6
  "tools": {
7
7
  "count": 67,
8
8
  "published_count": 65,
@@ -221,7 +221,7 @@ export const FACTS = {
221
221
  "workflow_model": "task-based",
222
222
  "tier": "A",
223
223
  "has_mcp": true,
224
- "has_hooks": false,
224
+ "has_hooks": true,
225
225
  "has_skills": true,
226
226
  "has_rules": true,
227
227
  "instruction_file": "AGENTS.md",
@@ -474,7 +474,7 @@ export const FACTS = {
474
474
  },
475
475
  "bench": {
476
476
  "schema": "brainclaw.bench.v1",
477
- "generated_at": "2026-07-18T18:21:18.026Z",
477
+ "generated_at": "2026-07-31T14:51:42.799Z",
478
478
  "node_version": "v24.18.0",
479
479
  "platform": "linux-x64",
480
480
  "repeats": 3,
@@ -483,7 +483,7 @@ export const FACTS = {
483
483
  "name": "cold_onboard",
484
484
  "volume": "empty",
485
485
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
486
- "duration_ms_median": 82,
486
+ "duration_ms_median": 81,
487
487
  "payload_chars_median": 1640,
488
488
  "payload_tokens_est_median": 410
489
489
  },
@@ -491,7 +491,7 @@ export const FACTS = {
491
491
  "name": "warm_work",
492
492
  "volume": "medium",
493
493
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
494
- "duration_ms_median": 146,
494
+ "duration_ms_median": 142,
495
495
  "payload_chars_median": 2626,
496
496
  "payload_tokens_est_median": 657
497
497
  },
@@ -499,9 +499,9 @@ export const FACTS = {
499
499
  "name": "first_edit",
500
500
  "volume": "medium",
501
501
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
502
- "duration_ms_median": 9,
503
- "payload_chars_median": 442,
504
- "payload_tokens_est_median": 111
502
+ "duration_ms_median": 12,
503
+ "payload_chars_median": 499,
504
+ "payload_tokens_est_median": 125
505
505
  }
506
506
  ]
507
507
  }
package/dist/facts.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.16.0",
3
- "generated_at": "2026-07-18T18:21:20.312Z",
2
+ "version": "1.18.0",
3
+ "generated_at": "2026-07-31T14:51:44.969Z",
4
4
  "tools": {
5
5
  "count": 67,
6
6
  "published_count": 65,
@@ -219,7 +219,7 @@
219
219
  "workflow_model": "task-based",
220
220
  "tier": "A",
221
221
  "has_mcp": true,
222
- "has_hooks": false,
222
+ "has_hooks": true,
223
223
  "has_skills": true,
224
224
  "has_rules": true,
225
225
  "instruction_file": "AGENTS.md",
@@ -472,7 +472,7 @@
472
472
  },
473
473
  "bench": {
474
474
  "schema": "brainclaw.bench.v1",
475
- "generated_at": "2026-07-18T18:21:18.026Z",
475
+ "generated_at": "2026-07-31T14:51:42.799Z",
476
476
  "node_version": "v24.18.0",
477
477
  "platform": "linux-x64",
478
478
  "repeats": 3,
@@ -481,7 +481,7 @@
481
481
  "name": "cold_onboard",
482
482
  "volume": "empty",
483
483
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
484
- "duration_ms_median": 82,
484
+ "duration_ms_median": 81,
485
485
  "payload_chars_median": 1640,
486
486
  "payload_tokens_est_median": 410
487
487
  },
@@ -489,7 +489,7 @@
489
489
  "name": "warm_work",
490
490
  "volume": "medium",
491
491
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
492
- "duration_ms_median": 146,
492
+ "duration_ms_median": 142,
493
493
  "payload_chars_median": 2626,
494
494
  "payload_tokens_est_median": 657
495
495
  },
@@ -497,9 +497,9 @@
497
497
  "name": "first_edit",
498
498
  "volume": "medium",
499
499
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
500
- "duration_ms_median": 9,
501
- "payload_chars_median": 442,
502
- "payload_tokens_est_median": 111
500
+ "duration_ms_median": 12,
501
+ "payload_chars_median": 499,
502
+ "payload_tokens_est_median": 125
503
503
  }
504
504
  ]
505
505
  }
Binary file
Binary file
Binary file
Binary file
Binary file
package/docs/cli.md CHANGED
@@ -2011,7 +2011,7 @@ The default catalog is intentionally small and centred on the canonical grammar.
2011
2011
  |---|---|
2012
2012
  | `bclaw_coordinate(intent)` | Assign, consult, review, reroute, or summarize across agents. Pass `open_loop: true` on `intent="review"` to also dispatch the reviewer turn. |
2013
2013
  | `bclaw_dispatch(intent)` | Parallelize execute across a sequence's lanes (analysis / execute / review). |
2014
- | `bclaw_loop(intent)` | Drive a turn in an existing multi-turn loop (`turn`, `complete_turn`, `advance`, `close`). Do not call `bclaw_loop(intent="open")` directly without dispatch — use `bclaw_coordinate(intent="review", open_loop: true)` instead. |
2014
+ | `bclaw_loop(intent)` | Drive a turn in an existing multi-turn loop (`turn`, `complete_turn`, `advance`, `close`; implementation loops add `bind` to dispatch the linked sequence and `verify` to run the opener-configured `command_green` check). Do not call `bclaw_loop(intent="open")` directly without dispatch — use `bclaw_coordinate(intent="review", open_loop: true)` instead. |
2015
2015
 
2016
2016
  **Sequences**:
2017
2017
 
package/docs/code-map.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Code Map
2
2
 
3
- Code Map is a per-project structural index of your JavaScript / TypeScript / JSX /
4
- TSX, Python, PHP, and Java codebase. It parses each supported file with Tree-sitter and records the
3
+ Code Map is a per-project structural index of your codebase across 11 languages:
4
+ JavaScript / TypeScript (including JSX / TSX), Python, PHP, Java, Go, Rust, C#,
5
+ Ruby, C, and C++. It parses each supported file with Tree-sitter and records the
5
6
  symbols it defines (functions, classes, types, interfaces, React components and
6
7
  hooks), what it imports and exports, and how files relate — then answers fast
7
8
  "what should I read before I edit this?" questions for both human operators and
@@ -201,11 +202,24 @@ single-project repos ignore the flag entirely.
201
202
  which nested projects have a built index vs `missing_index`, plus an aggregate
202
203
  count — so you can see workspace-wide freshness from the root.
203
204
 
205
+ ### Workspace-wide `find` / `brief`
206
+
207
+ Once the per-child indexes exist (built by `--cascade`), `find` and `brief` run
208
+ at a multi-project workspace **root** automatically aggregate across every child
209
+ project's store — no flag needed. Matches are project-tagged with
210
+ workspace-relative paths, and the freshness badge merges per-store status (worst
211
+ status wins) plus coverage (how many projects are indexed, listing any unindexed
212
+ children). An aggregated `brief` also surfaces **cross-package reverse
213
+ dependents**: sibling packages that import the defining package's public name
214
+ rank into the reading list, flagged `cross_package`.
215
+
216
+ From **inside** a child project, reads stay single-store by default (locality).
217
+ An explicit `traversal: "workspace"` (backend option) walks up to the nearest
218
+ enclosing multi-project root and aggregates from there, with the caller's own
219
+ package ranked first (`local: true` on its rows).
220
+
204
221
  **Not yet supported** (roadmap):
205
222
 
206
- - A single **federated query** at the root that fans out across the per-child
207
- indexes and merges the results (today, `--cascade` builds the per-child indexes;
208
- `find` / `brief` still run against one store at a time).
209
223
  - **Cross-service edges** — e.g. linking an API call to the route that defines it in
210
224
  another service. Code Map indexes language *symbols* and *module imports*, not
211
225
  framework routes or runtime HTTP calls, so it does not (today) map "service A calls
@@ -215,7 +229,9 @@ count — so you can see workspace-wide freshness from the root.
215
229
 
216
230
  The parser is [Tree-sitter](https://tree-sitter.github.io/) compiled to
217
231
  WebAssembly. The engine glue (`web-tree-sitter`) and the prebuilt grammar `.wasm`
218
- files (JavaScript / TypeScript / JSX / TSX, Python, PHP, Java) are **bundled into the package** during the
232
+ files — 12 grammars covering the 11 supported languages: `javascript` (also
233
+ handles JSX), `typescript`, `tsx`, `python`, `php`, `java`, `go`, `rust`,
234
+ `c_sharp`, `ruby`, `c`, `cpp` — are **bundled into the package** during the
219
235
  build (`scripts/copy-code-map-wasm.mjs` copies them into `dist/wasm/` and vendors
220
236
  the engine glue into `dist/vendor/web-tree-sitter/`).
221
237