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.
- package/README.md +22 -8
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-review.js +2 -2
- package/dist/commands/code-map.js +4 -1
- package/dist/commands/codev.js +61 -30
- package/dist/commands/doctor.js +14 -1
- package/dist/commands/harvest.js +241 -25
- package/dist/commands/inbox.js +10 -4
- package/dist/commands/loop.js +2 -2
- package/dist/commands/loops-handlers.js +82 -1
- package/dist/commands/mcp-catalog.js +12 -4
- package/dist/commands/mcp-read-handlers.js +90 -7
- package/dist/commands/mcp-schemas.generated.js +3 -0
- package/dist/commands/mcp-write-coordination.js +159 -40
- package/dist/commands/mcp.js +11 -2
- package/dist/core/agent-capability.js +7 -2
- package/dist/core/agent-files.js +53 -2
- package/dist/core/agent-integrations.js +1 -0
- package/dist/core/agentrun-reconciler.js +171 -7
- package/dist/core/agentruns.js +6 -1
- package/dist/core/code-map/aggregate.js +473 -0
- package/dist/core/code-map/backend.js +36 -10
- package/dist/core/code-map/freshness.js +36 -1
- package/dist/core/code-map/lang/c/imports.scm +12 -0
- package/dist/core/code-map/lang/c/index.js +150 -0
- package/dist/core/code-map/lang/c/tags.scm +68 -0
- package/dist/core/code-map/lang/cpp/imports.scm +14 -0
- package/dist/core/code-map/lang/cpp/index.js +149 -0
- package/dist/core/code-map/lang/cpp/tags.scm +87 -0
- package/dist/core/code-map/lang/csharp/imports.scm +20 -0
- package/dist/core/code-map/lang/csharp/index.js +224 -0
- package/dist/core/code-map/lang/csharp/tags.scm +63 -0
- package/dist/core/code-map/lang/go/imports.scm +13 -0
- package/dist/core/code-map/lang/go/index.js +139 -0
- package/dist/core/code-map/lang/go/tags.scm +36 -0
- package/dist/core/code-map/lang/providers.js +12 -1
- package/dist/core/code-map/lang/ruby/imports.scm +24 -0
- package/dist/core/code-map/lang/ruby/index.js +198 -0
- package/dist/core/code-map/lang/ruby/tags.scm +49 -0
- package/dist/core/code-map/lang/rust/imports.scm +44 -0
- package/dist/core/code-map/lang/rust/index.js +136 -0
- package/dist/core/code-map/lang/rust/tags.scm +47 -0
- package/dist/core/code-map/query.js +229 -80
- package/dist/core/code-map/types.js +18 -0
- package/dist/core/code-map/work-section.js +8 -7
- package/dist/core/codev-responses.js +16 -0
- package/dist/core/dispatcher.js +209 -29
- package/dist/core/execution-adapters.js +29 -3
- package/dist/core/ideation-loop-close.js +124 -0
- package/dist/core/loops/artifact-resolver.js +197 -0
- package/dist/core/loops/attempt-reservation.js +576 -0
- package/dist/core/loops/commit-intent.js +494 -0
- package/dist/core/loops/facade-schema.js +48 -0
- package/dist/core/loops/impl-bind.js +144 -0
- package/dist/core/loops/index.js +1 -1
- package/dist/core/loops/iteration-engine.js +29 -0
- package/dist/core/loops/lock.js +14 -0
- package/dist/core/loops/project-resolution.js +157 -0
- package/dist/core/loops/reconcile-turn.js +369 -0
- package/dist/core/loops/result-reducers.js +88 -0
- package/dist/core/loops/store.js +46 -7
- package/dist/core/loops/types.js +139 -11
- package/dist/core/loops/verbs.js +9 -3
- package/dist/core/loops/verify-command.js +209 -0
- package/dist/core/messaging.js +58 -5
- package/dist/core/review-loop-close.js +106 -34
- package/dist/core/review-loop-turn-dispatch.js +445 -0
- package/dist/core/runtime-signals.js +68 -0
- package/dist/core/schema.js +34 -0
- package/dist/core/worktree.js +240 -22
- package/dist/facts.js +10 -10
- package/dist/facts.json +9 -9
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/docs/cli.md +1 -1
- package/docs/code-map.md +22 -6
- package/docs/concepts/loop-engine.md +28 -2
- package/docs/concepts/observer-protocol.md +22 -0
- package/docs/integrations/codex.md +19 -3
- package/docs/mcp-schema-changelog.md +43 -1
- 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
|
package/dist/core/schema.js
CHANGED
|
@@ -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
|
package/dist/core/worktree.js
CHANGED
|
@@ -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.
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
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
|
|
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
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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.
|
|
2
|
+
// Source: brainclaw v1.18.0 on 2026-07-31T14:51:44.969Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.
|
|
5
|
-
"generated_at": "2026-07-
|
|
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":
|
|
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-
|
|
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":
|
|
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":
|
|
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":
|
|
503
|
-
"payload_chars_median":
|
|
504
|
-
"payload_tokens_est_median":
|
|
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.
|
|
3
|
-
"generated_at": "2026-07-
|
|
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":
|
|
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-
|
|
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":
|
|
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":
|
|
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":
|
|
501
|
-
"payload_chars_median":
|
|
502
|
-
"payload_tokens_est_median":
|
|
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
|
|
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
|
|
4
|
-
TSX, Python, PHP,
|
|
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
|
|
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
|
|