brainclaw 1.28.2 → 1.28.3
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/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/mcp-catalog.js +2 -2
- package/dist/commands/mcp-write-coordination.js +13 -4
- package/dist/commands/mcp-write-entities.js +5 -2
- package/dist/commands/mcp.js +25 -7
- package/dist/core/code-map/aggregate.js +20 -7
- package/dist/core/code-map/backend.js +17 -7
- package/dist/core/code-map/cascade-jobs.js +174 -0
- package/dist/core/code-map/cascade-worker.js +15 -0
- package/dist/core/code-map/cascade.js +63 -26
- package/dist/core/code-map/query.js +6 -3
- package/dist/core/entity-operations.js +18 -4
- package/dist/core/execution-adapters.js +8 -1
- package/dist/core/spawn-check.js +9 -1
- package/dist/facts.js +7 -7
- package/dist/facts.json +6 -6
- package/docs/cli.md +2 -2
- package/docs/code-map.md +20 -9
- package/docs/integrations/mcp.md +2 -2
- package/docs/mcp-schema-changelog.md +30 -0
- package/package.json +1 -1
|
Binary file
|
|
@@ -374,7 +374,7 @@ export const MCP_READ_TOOLS = [
|
|
|
374
374
|
},
|
|
375
375
|
{
|
|
376
376
|
name: 'bclaw_code_status',
|
|
377
|
-
description: 'Code Map status for
|
|
377
|
+
description: 'Code Map status for the active session project: store presence, freshness badge, and index stats. Read-only; never refreshes. In a multi-project workspace, cascade=true adds per-child coverage plus progress/terminal diagnostics for the latest durable cascade job.',
|
|
378
378
|
annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'auto' },
|
|
379
379
|
inputSchema: {
|
|
380
380
|
type: 'object',
|
|
@@ -459,7 +459,7 @@ export const MCP_READ_TOOLS = [
|
|
|
459
459
|
const MCP_WRITE_TOOLS = [
|
|
460
460
|
{
|
|
461
461
|
name: 'bclaw_code_refresh',
|
|
462
|
-
description: 'Rebuild the Code Map index for
|
|
462
|
+
description: 'Rebuild the Code Map index for the active session project. scope="changed" (default) reparses changed files; scope="all" does a full refresh + compaction. In a multi-project workspace, cascade=true starts a durable background job immediately; follow it with bclaw_code_status(cascade=true), which reports progress and terminal per-project diagnostics without an MCP timeout.',
|
|
463
463
|
annotations: { tier: 'standard', category: 'discovery', headlessApproval: 'prompt' },
|
|
464
464
|
inputSchema: {
|
|
465
465
|
type: 'object',
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
import crypto from 'node:crypto';
|
|
15
|
+
import path from 'node:path';
|
|
15
16
|
import { spawnSync } from 'node:child_process';
|
|
16
17
|
import { buildClaimEnvPrefix } from '../core/execution-profile.js';
|
|
17
18
|
import { resolveProjectCwd } from '../core/cross-project.js';
|
|
@@ -387,9 +388,17 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
387
388
|
// for state-mutating helpers; the outer `cwd` (source) stays in scope
|
|
388
389
|
// for the few cases that genuinely need source attribution.
|
|
389
390
|
const dispatchCwd = resolveProjectCwd(req.project, cwd);
|
|
390
|
-
const isCrossProject = dispatchCwd !== cwd;
|
|
391
|
+
const isCrossProject = path.resolve(dispatchCwd) !== path.resolve(cwd);
|
|
391
392
|
if (isCrossProject && req.autoExecute !== false) {
|
|
392
|
-
|
|
393
|
+
return {
|
|
394
|
+
response: createToolErrorResponse('cross_project_auto_execute_unsupported', `cross-project dispatch (project='${req.project}') cannot auto-execute from the source process; admission refused before creating a claim, assignment, or loop.`, {
|
|
395
|
+
next_actions: [{
|
|
396
|
+
tool: 'bclaw_coordinate',
|
|
397
|
+
args: { ...req, autoExecute: false },
|
|
398
|
+
when: 'create an inbox-only cross-project assignment that the target agent will pick up with bclaw_work',
|
|
399
|
+
}],
|
|
400
|
+
}),
|
|
401
|
+
};
|
|
393
402
|
}
|
|
394
403
|
const effectiveAutoExecute = isCrossProject ? false : req.autoExecute;
|
|
395
404
|
// pln#692 P0 — an explicit checkout ref is part of admission, not worktree
|
|
@@ -1008,7 +1017,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1008
1017
|
// existing length===0 guard skips loop creation. Skipped when open_loop
|
|
1009
1018
|
// is off, preflight=false, or BRAINCLAW_NO_SPAWN is set (handled inside
|
|
1010
1019
|
// preflightAgents). Cross-project dispatch never auto-spawns, so skip.
|
|
1011
|
-
if (req.open_loop === true && req.preflight !== false && !
|
|
1020
|
+
if (req.open_loop === true && req.preflight !== false && !isCrossProject && loopReviewerAgents.length > 0) {
|
|
1012
1021
|
try {
|
|
1013
1022
|
const { preflightAgents } = await import('../core/spawn-check.js');
|
|
1014
1023
|
const pf = await preflightAgents(loopReviewerAgents, { cwd: dispatchCwd });
|
|
@@ -1158,7 +1167,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1158
1167
|
// (that is the double-spawn hole), and do NOT release the (possibly shared) claim; leave
|
|
1159
1168
|
// the slot for reconcile/self-heal. LEGACY: the unchanged inline mint runs.
|
|
1160
1169
|
let usedTurnOwned = false;
|
|
1161
|
-
if (turnOwnedReviewEnabled() && !
|
|
1170
|
+
if (turnOwnedReviewEnabled() && !isCrossProject) {
|
|
1162
1171
|
const prep = prepareTurnOwnedReviewDispatch({
|
|
1163
1172
|
loopId: loop.id,
|
|
1164
1173
|
slotId: slot.slot_id,
|
|
@@ -479,9 +479,12 @@ export function handleBclawCreate(payload, ctx) {
|
|
|
479
479
|
const result = createEntity(entity, data, targetCwd);
|
|
480
480
|
appendAuditEntry({ actor: actor ?? 'unknown', ...(actorId ? { actor_id: actorId } : {}), action: 'create', item_id: result.id, item_type: entity }, targetCwd);
|
|
481
481
|
const createText = `✔ created ${entity} ${result.id}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}`;
|
|
482
|
+
const proximityText = result.nearby_items?.length
|
|
483
|
+
? `Nearby existing ${entity} item(s): ${result.nearby_items.map((item) => `${item.id} (${item.reason})`).join(', ')}. Creation was kept; review before adding another duplicate.`
|
|
484
|
+
: undefined;
|
|
482
485
|
const createContent = autoRepair
|
|
483
|
-
? [{ type: 'text', text: createText }, { type: 'text', text: renderAutoRepairWarning(autoRepair, actor ?? 'unknown') }]
|
|
484
|
-
: [{ type: 'text', text: createText }];
|
|
486
|
+
? [{ type: 'text', text: createText }, { type: 'text', text: renderAutoRepairWarning(autoRepair, actor ?? 'unknown') }, ...(proximityText ? [{ type: 'text', text: proximityText }] : [])]
|
|
487
|
+
: [{ type: 'text', text: createText }, ...(proximityText ? [{ type: 'text', text: proximityText }] : [])];
|
|
485
488
|
// pln#634 — a freshly created plan whose steps are never added is the most
|
|
486
489
|
// common half-finished shape in the store; a sequence with no readiness
|
|
487
490
|
// check is the second. Only those two emit a follow-up.
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1096,8 +1096,10 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1096
1096
|
if (name === 'bclaw_code_status' || name === 'bclaw_code_find' || name === 'bclaw_code_brief' || name === 'bclaw_code_impact' || name === 'bclaw_code_export' || name === 'bclaw_code_outline' || name === 'bclaw_code_refresh') {
|
|
1097
1097
|
const { JsonlBackend } = await import('../core/code-map/backend.js');
|
|
1098
1098
|
const be = new JsonlBackend();
|
|
1099
|
+
// Session-scoped project selection is authoritative for Code Map too.
|
|
1100
|
+
const codeCwd = scopeInfo.cwd;
|
|
1099
1101
|
if (name === 'bclaw_code_status') {
|
|
1100
|
-
const status = await be.status({ cwd, cascade: args.cascade === true });
|
|
1102
|
+
const status = await be.status({ cwd: codeCwd, cascade: args.cascade === true });
|
|
1101
1103
|
const diskVersion = readDiskBrainclawVersion();
|
|
1102
1104
|
return {
|
|
1103
1105
|
response: toolResponse({
|
|
@@ -1118,7 +1120,23 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1118
1120
|
}
|
|
1119
1121
|
if (name === 'bclaw_code_refresh') {
|
|
1120
1122
|
const scope = args.scope === 'all' ? 'all' : 'changed';
|
|
1121
|
-
|
|
1123
|
+
if (args.cascade === true) {
|
|
1124
|
+
const { startCascadeRefreshJob, summarizeCascadeRefreshJob } = await import('../core/code-map/cascade-jobs.js');
|
|
1125
|
+
const job = startCascadeRefreshJob(codeCwd, scope);
|
|
1126
|
+
if (job) {
|
|
1127
|
+
return {
|
|
1128
|
+
response: toolResponse({
|
|
1129
|
+
content: [{ type: 'text', text: `Code Map cascade started: job=${job.job_id}, projects=${job.projects_total}. Follow with bclaw_code_status(cascade=true).` }],
|
|
1130
|
+
structuredContent: {
|
|
1131
|
+
started: true,
|
|
1132
|
+
...summarizeCascadeRefreshJob(job),
|
|
1133
|
+
next_actions: [{ tool: 'bclaw_code_status', args: { cascade: true }, when: 'follow progress and terminal per-project diagnostics' }],
|
|
1134
|
+
},
|
|
1135
|
+
}),
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const result = await be.refresh({ scope, cwd: codeCwd, cascade: args.cascade === true });
|
|
1122
1140
|
const cascadeNote = result.cascade ? ` cascade=${result.cascade.children_refreshed} child(ren)+root` : '';
|
|
1123
1141
|
return {
|
|
1124
1142
|
response: toolResponse({
|
|
@@ -1133,7 +1151,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1133
1151
|
return { response: createToolErrorResponse('validation_error', 'bclaw_code_find requires a non-empty query.') };
|
|
1134
1152
|
}
|
|
1135
1153
|
const limit = typeof args.limit === 'number' ? args.limit : undefined;
|
|
1136
|
-
const result = await be.find({ query, limit, cwd });
|
|
1154
|
+
const result = await be.find({ query, limit, cwd: codeCwd });
|
|
1137
1155
|
return {
|
|
1138
1156
|
response: toolResponse({
|
|
1139
1157
|
content: [{ type: 'text', text: `Code Map find "${result.query}": ${result.matches.length} match(es), freshness=${result.freshness_badge.freshness}` }],
|
|
@@ -1154,7 +1172,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1154
1172
|
const maxEdges = typeof args.maxEdges === 'number' ? args.maxEdges : undefined;
|
|
1155
1173
|
const minConfidence = typeof args.minConfidence === 'number' ? args.minConfidence : undefined;
|
|
1156
1174
|
const format = args.format === 'mermaid' ? 'mermaid' : args.format === 'json' ? 'json' : undefined;
|
|
1157
|
-
const result = await be.exportGraph({ target, targetKind, direction, depth, maxNodes, maxEdges, minConfidence, format, cwd });
|
|
1175
|
+
const result = await be.exportGraph({ target, targetKind, direction, depth, maxNodes, maxEdges, minConfidence, format, cwd: codeCwd });
|
|
1158
1176
|
return {
|
|
1159
1177
|
response: toolResponse({
|
|
1160
1178
|
content: [{ type: 'text', text: `Code Map export "${result.target}": ${result.nodes.length} node(s), ${result.edges.length} edge(s), depth=${result.limits.max_depth}, freshness=${result.freshness_badge.freshness}` }],
|
|
@@ -1170,7 +1188,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1170
1188
|
}
|
|
1171
1189
|
const depth = typeof args.depth === 'number' ? args.depth : undefined;
|
|
1172
1190
|
const limit = typeof args.limit === 'number' ? args.limit : undefined;
|
|
1173
|
-
const result = await be.impact({ target, depth, limit, cwd });
|
|
1191
|
+
const result = await be.impact({ target, depth, limit, cwd: codeCwd });
|
|
1174
1192
|
return {
|
|
1175
1193
|
response: toolResponse({
|
|
1176
1194
|
content: [{ type: 'text', text: `Code Map impact "${result.target}": ${result.risk.counters.direct_dependents} direct, ${result.risk.counters.transitive_dependents} transitive dependent(s), risk=${result.risk.score}, freshness=${result.freshness_badge.freshness}` }],
|
|
@@ -1185,7 +1203,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1185
1203
|
return { response: createToolErrorResponse('validation_error', 'bclaw_code_outline requires a non-empty path.') };
|
|
1186
1204
|
}
|
|
1187
1205
|
const limit = typeof args.limit === 'number' ? args.limit : undefined;
|
|
1188
|
-
const result = await be.outline({ path: outlinePath, limit, cwd });
|
|
1206
|
+
const result = await be.outline({ path: outlinePath, limit, cwd: codeCwd });
|
|
1189
1207
|
return {
|
|
1190
1208
|
response: toolResponse({
|
|
1191
1209
|
content: [{ type: 'text', text: `Code Map outline "${result.path}": ${result.symbols.length}/${result.symbol_count} symbol(s), index=${result.index_status}, freshness=${result.freshness_badge.freshness}` }],
|
|
@@ -1199,7 +1217,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1199
1217
|
return { response: createToolErrorResponse('validation_error', 'bclaw_code_brief requires a non-empty target.') };
|
|
1200
1218
|
}
|
|
1201
1219
|
const limit = typeof args.limit === 'number' ? args.limit : undefined;
|
|
1202
|
-
const result = await be.brief({ target, limit, cwd });
|
|
1220
|
+
const result = await be.brief({ target, limit, cwd: codeCwd });
|
|
1203
1221
|
return {
|
|
1204
1222
|
response: toolResponse({
|
|
1205
1223
|
content: [{ type: 'text', text: `Code Map brief "${result.target}": ${result.suggested_files_to_read.length} file(s) to read, freshness=${result.freshness_badge.freshness}` }],
|
|
@@ -183,10 +183,9 @@ function statusRank(s) {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
/**
|
|
186
|
-
* Merge per-store badges into one workspace badge
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* un-indexed is the whole workspace `missing_index`.
|
|
186
|
+
* Merge per-store badges into one workspace badge. Missing child indexes are a
|
|
187
|
+
* PARTIAL workspace, never a fresh one: serving the indexed subset is useful,
|
|
188
|
+
* but the top-line signal must describe the coverage actually searched.
|
|
190
189
|
*/
|
|
191
190
|
function mergeBadges(perStore) {
|
|
192
191
|
const total = perStore.length;
|
|
@@ -202,14 +201,28 @@ function mergeBadges(perStore) {
|
|
|
202
201
|
if (statusRank(p.badge.status) > statusRank(worst))
|
|
203
202
|
worst = p.badge.status;
|
|
204
203
|
}
|
|
204
|
+
if (unindexed.length > 0)
|
|
205
|
+
worst = 'partial';
|
|
206
|
+
const statusCounts = {};
|
|
207
|
+
for (const p of perStore) {
|
|
208
|
+
const status = p.hasIndex ? p.badge.status : 'missing_index';
|
|
209
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
210
|
+
}
|
|
211
|
+
const exceptionalProjects = perStore
|
|
212
|
+
.filter((p) => p.hasIndex && p.badge.status !== 'fresh')
|
|
213
|
+
.map((p) => ({ path: p.ref.relPath || '.', status: p.badge.status }));
|
|
205
214
|
const details = {
|
|
206
215
|
traversal: 'workspace',
|
|
207
216
|
projects_indexed: indexed.length,
|
|
208
217
|
projects_total: total,
|
|
209
|
-
|
|
218
|
+
project_status_counts: statusCounts,
|
|
210
219
|
};
|
|
211
|
-
if (unindexed.length)
|
|
220
|
+
if (unindexed.length) {
|
|
221
|
+
details.unindexed_project_count = unindexed.length;
|
|
212
222
|
details.unindexed_projects = unindexed;
|
|
223
|
+
}
|
|
224
|
+
if (exceptionalProjects.length)
|
|
225
|
+
details.non_fresh_projects = exceptionalProjects;
|
|
213
226
|
const prefixMerge = (key) => {
|
|
214
227
|
const out = [];
|
|
215
228
|
for (const p of indexed) {
|
|
@@ -259,7 +272,7 @@ export function aggregateFind(query, limit, resolved, currentHead) {
|
|
|
259
272
|
const r = findInStore(query, { cwd: ref.cwd }, checker, acc);
|
|
260
273
|
// Per-store badge: drive `partial` from THIS store's own budget-skips (review F2),
|
|
261
274
|
// NOT the shared checker.exhausted flag — else an early store spending the budget
|
|
262
|
-
// would mislabel every fully-fresh later store as `partial` in
|
|
275
|
+
// would mislabel every fully-fresh later store as `partial` in diagnostics. Then
|
|
263
276
|
// apply per-store HEAD drift against the one workspace HEAD (review F3) so a child
|
|
264
277
|
// whose index lags the working tree is flagged even under an otherwise-fresh root.
|
|
265
278
|
let badge = deriveBadge(r.base, acc, false, r.matches.length > 0, r.emptyCandidates);
|
|
@@ -19,7 +19,8 @@ import { exportSubgraph } from './export.js';
|
|
|
19
19
|
import { fileId } from './ids.js';
|
|
20
20
|
import { resolveTraversal, aggregateFind, aggregateBrief } from './aggregate.js';
|
|
21
21
|
import { defaultMemoryReader } from './memory-reader.js';
|
|
22
|
-
import {
|
|
22
|
+
import { inspectNestedProjects, refreshWorkspaceCascade } from './cascade.js';
|
|
23
|
+
import { latestCascadeRefreshJob, summarizeCascadeRefreshJob } from './cascade-jobs.js';
|
|
23
24
|
import { loadConfig } from '../config.js';
|
|
24
25
|
import { codeMapDir } from './paths.js';
|
|
25
26
|
/** spec §9 caps the brief reading list at 12 files. */
|
|
@@ -112,17 +113,26 @@ function isMultiProjectWorkspace(cwd) {
|
|
|
112
113
|
/** Per-child store recap for `status(cascade)` in a multi-project workspace. */
|
|
113
114
|
function buildCascadeStatus(rootCwd) {
|
|
114
115
|
const root = rootCwd ?? process.cwd();
|
|
115
|
-
const
|
|
116
|
+
const discovery = inspectNestedProjects(root);
|
|
117
|
+
const children = discovery.projects.map((abs) => {
|
|
116
118
|
const m = readManifest(abs);
|
|
117
119
|
return {
|
|
118
120
|
path: path.relative(root, abs).replace(/\\/g, '/') || '.',
|
|
119
121
|
store_exists: m ? true : storeExists(abs),
|
|
120
122
|
freshness: m ? m.freshness.status : 'missing_index',
|
|
121
123
|
files_indexed: m ? m.stats.files_indexed : null,
|
|
124
|
+
...(m && m.stats.files_indexed === 0 ? { reason: 'no_eligible_files' } : {}),
|
|
122
125
|
};
|
|
123
126
|
});
|
|
124
127
|
const indexed = children.filter((c) => c.freshness !== 'missing_index').length;
|
|
125
|
-
|
|
128
|
+
const latestJob = latestCascadeRefreshJob(root);
|
|
129
|
+
return {
|
|
130
|
+
children,
|
|
131
|
+
indexed_children: indexed,
|
|
132
|
+
total_children: children.length,
|
|
133
|
+
discovery_truncated: discovery.truncated,
|
|
134
|
+
...(latestJob ? { refresh_job: summarizeCascadeRefreshJob(latestJob) } : {}),
|
|
135
|
+
};
|
|
126
136
|
}
|
|
127
137
|
/**
|
|
128
138
|
* P0 JSONL-backed query backend. Reads the durable file store (manifest +
|
|
@@ -198,17 +208,17 @@ export class JsonlBackend {
|
|
|
198
208
|
// A cascade is only fully "acquired" when EVERY project got its lock; if a
|
|
199
209
|
// child or the root was skipped under a live writer, surface that instead
|
|
200
210
|
// of reporting a clean lock_acquired=true over a partial cascade (codex review).
|
|
201
|
-
const
|
|
211
|
+
const incomplete = allProjects.filter((p) => p.outcome === 'locked' || p.outcome === 'failed');
|
|
202
212
|
return {
|
|
203
213
|
ran: allProjects.some((p) => p.ran),
|
|
204
214
|
scope,
|
|
205
|
-
lock_acquired:
|
|
215
|
+
lock_acquired: incomplete.length === 0,
|
|
206
216
|
freshness_badge: badge(root.freshness, {
|
|
207
217
|
files_parsed: root.files_parsed,
|
|
208
218
|
children_refreshed: cascade.children_refreshed,
|
|
209
219
|
}),
|
|
210
|
-
...(
|
|
211
|
-
? { lock_status: `${
|
|
220
|
+
...(incomplete.length > 0
|
|
221
|
+
? { lock_status: `${incomplete.length} project(s) incomplete: ${incomplete.map((p) => `${p.path} (${p.outcome})`).join(', ')}` }
|
|
212
222
|
: {}),
|
|
213
223
|
cascade,
|
|
214
224
|
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { writeFileAtomic } from '../io.js';
|
|
8
|
+
import { codeMapDir } from './paths.js';
|
|
9
|
+
import { inspectNestedProjects, refreshWorkspaceCascade } from './cascade.js';
|
|
10
|
+
export function summarizeCascadeRefreshJob(job) {
|
|
11
|
+
const { result, ...summary } = job;
|
|
12
|
+
if (!result)
|
|
13
|
+
return summary;
|
|
14
|
+
const projects = [result.root_result, ...result.children];
|
|
15
|
+
const outcomeCounts = {};
|
|
16
|
+
for (const project of projects)
|
|
17
|
+
outcomeCounts[project.outcome] = (outcomeCounts[project.outcome] ?? 0) + 1;
|
|
18
|
+
const problemProjects = projects
|
|
19
|
+
.filter((project) => project.outcome !== 'indexed')
|
|
20
|
+
.map((project) => ({ path: project.path, outcome: project.outcome, ...(project.reason ? { reason: project.reason } : {}), ...(project.error ? { error: project.error } : {}) }));
|
|
21
|
+
return { ...summary, outcome_counts: outcomeCounts, ...(problemProjects.length ? { problem_projects: problemProjects } : {}) };
|
|
22
|
+
}
|
|
23
|
+
function jobsDir(root) {
|
|
24
|
+
return path.join(codeMapDir(root), 'cascade-jobs');
|
|
25
|
+
}
|
|
26
|
+
function jobPath(root, jobId) {
|
|
27
|
+
return path.join(jobsDir(root), `${jobId}.json`);
|
|
28
|
+
}
|
|
29
|
+
function writeJob(job) {
|
|
30
|
+
const dir = jobsDir(job.root);
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
const target = jobPath(job.root, job.job_id);
|
|
33
|
+
writeFileAtomic(target, `${JSON.stringify(job, null, 2)}\n`);
|
|
34
|
+
}
|
|
35
|
+
export function readCascadeRefreshJob(root, jobId) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(jobPath(path.resolve(root), jobId), 'utf8'));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function latestCascadeRefreshJob(root) {
|
|
44
|
+
const dir = jobsDir(path.resolve(root));
|
|
45
|
+
try {
|
|
46
|
+
const jobs = fs.readdirSync(dir)
|
|
47
|
+
.filter((name) => name.endsWith('.json'))
|
|
48
|
+
.map((name) => {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.filter((job) => job !== null)
|
|
57
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
58
|
+
return jobs[0] ?? null;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function processAlive(pid) {
|
|
65
|
+
if (!pid)
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, 0);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function startCascadeRefreshJob(root, scope) {
|
|
76
|
+
const resolvedRoot = path.resolve(root);
|
|
77
|
+
try {
|
|
78
|
+
if (loadConfig(resolvedRoot).project_mode !== 'multi-project')
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const previous = latestCascadeRefreshJob(resolvedRoot);
|
|
85
|
+
if (previous && (previous.status === 'queued' || previous.status === 'running') && processAlive(previous.pid)) {
|
|
86
|
+
return previous;
|
|
87
|
+
}
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
const job = {
|
|
90
|
+
job_id: `cmj_${crypto.randomBytes(8).toString('hex')}`,
|
|
91
|
+
root: resolvedRoot,
|
|
92
|
+
scope,
|
|
93
|
+
status: 'queued',
|
|
94
|
+
created_at: now,
|
|
95
|
+
updated_at: now,
|
|
96
|
+
projects_total: inspectNestedProjects(resolvedRoot).projects.length + 1,
|
|
97
|
+
projects_completed: 0,
|
|
98
|
+
current_project: null,
|
|
99
|
+
};
|
|
100
|
+
writeJob(job);
|
|
101
|
+
const worker = fileURLToPath(new URL('./cascade-worker.js', import.meta.url));
|
|
102
|
+
const child = spawn(process.execPath, [worker, resolvedRoot, job.job_id, scope], {
|
|
103
|
+
cwd: resolvedRoot,
|
|
104
|
+
detached: true,
|
|
105
|
+
stdio: 'ignore',
|
|
106
|
+
windowsHide: true,
|
|
107
|
+
});
|
|
108
|
+
const markWorkerExit = (detail) => {
|
|
109
|
+
const current = readCascadeRefreshJob(resolvedRoot, job.job_id);
|
|
110
|
+
if (!current || current.status === 'completed' || current.status === 'failed')
|
|
111
|
+
return;
|
|
112
|
+
const completed = new Date().toISOString();
|
|
113
|
+
writeJob({
|
|
114
|
+
...current,
|
|
115
|
+
status: 'failed',
|
|
116
|
+
error: detail,
|
|
117
|
+
current_project: null,
|
|
118
|
+
completed_at: completed,
|
|
119
|
+
updated_at: completed,
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
child.once('error', (error) => {
|
|
123
|
+
markWorkerExit(`cascade worker failed to start: ${error.message}`);
|
|
124
|
+
});
|
|
125
|
+
child.once('exit', (code, signal) => {
|
|
126
|
+
// The worker writes its terminal state synchronously before exiting. If no
|
|
127
|
+
// terminal state exists here, the launch/runtime failed outside its guard.
|
|
128
|
+
markWorkerExit(`cascade worker exited before completion (code=${code ?? 'null'}, signal=${signal ?? 'none'})`);
|
|
129
|
+
});
|
|
130
|
+
child.unref();
|
|
131
|
+
const current = readCascadeRefreshJob(resolvedRoot, job.job_id) ?? job;
|
|
132
|
+
if (current.status === 'queued') {
|
|
133
|
+
current.pid = child.pid;
|
|
134
|
+
current.updated_at = new Date().toISOString();
|
|
135
|
+
writeJob(current);
|
|
136
|
+
}
|
|
137
|
+
return current;
|
|
138
|
+
}
|
|
139
|
+
/** Worker entry seam, exported for deterministic tests and the tiny worker file. */
|
|
140
|
+
export async function runCascadeRefreshJob(root, jobId, scope) {
|
|
141
|
+
const job = readCascadeRefreshJob(root, jobId);
|
|
142
|
+
if (!job)
|
|
143
|
+
throw new Error(`cascade job not found: ${jobId}`);
|
|
144
|
+
const started = new Date().toISOString();
|
|
145
|
+
writeJob({ ...job, status: 'running', pid: process.pid, started_at: started, updated_at: started });
|
|
146
|
+
try {
|
|
147
|
+
const result = await refreshWorkspaceCascade({
|
|
148
|
+
rootCwd: root,
|
|
149
|
+
scope,
|
|
150
|
+
onProgress: (progress) => {
|
|
151
|
+
const current = readCascadeRefreshJob(root, jobId) ?? job;
|
|
152
|
+
writeJob({
|
|
153
|
+
...current,
|
|
154
|
+
status: 'running',
|
|
155
|
+
pid: process.pid,
|
|
156
|
+
projects_total: progress.total,
|
|
157
|
+
projects_completed: progress.completed,
|
|
158
|
+
current_project: progress.current_project,
|
|
159
|
+
...(progress.last_result ? { last_result: progress.last_result } : {}),
|
|
160
|
+
updated_at: new Date().toISOString(),
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const current = readCascadeRefreshJob(root, jobId) ?? job;
|
|
165
|
+
const completed = new Date().toISOString();
|
|
166
|
+
writeJob({ ...current, status: 'completed', result, current_project: null, projects_completed: current.projects_total, completed_at: completed, updated_at: completed });
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const current = readCascadeRefreshJob(root, jobId) ?? job;
|
|
170
|
+
const completed = new Date().toISOString();
|
|
171
|
+
writeJob({ ...current, status: 'failed', error: error instanceof Error ? error.message : String(error), current_project: null, completed_at: completed, updated_at: completed });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=cascade-jobs.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { runCascadeRefreshJob } from './cascade-jobs.js';
|
|
2
|
+
const [root, jobId, rawScope] = process.argv.slice(2);
|
|
3
|
+
if (!root || !jobId)
|
|
4
|
+
process.exit(2);
|
|
5
|
+
const scope = rawScope === 'all' ? 'all' : 'changed';
|
|
6
|
+
try {
|
|
7
|
+
await runCascadeRefreshJob(root, jobId, scope);
|
|
8
|
+
// Tree-sitter/native handles can keep Node's event loop alive after the
|
|
9
|
+
// durable terminal record has been flushed. This process owns no other work.
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=cascade-worker.js.map
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* refreshes existing brainclaw projects, it does not initialise new ones.
|
|
21
21
|
*/
|
|
22
22
|
import path from 'node:path';
|
|
23
|
-
import {
|
|
23
|
+
import { scanNestedBrainclawProjectsDetailed } from '../workspace-projects.js';
|
|
24
24
|
import { loadConfig } from '../config.js';
|
|
25
25
|
import { refresh as runRefresh } from './refresh.js';
|
|
26
26
|
import { readManifest } from './store.js';
|
|
@@ -54,10 +54,14 @@ function projectIdFor(cwd, fallbackId) {
|
|
|
54
54
|
* cascade and the `status --cascade` recap so both agree on the project set.
|
|
55
55
|
*/
|
|
56
56
|
export function listNestedProjects(rootCwd) {
|
|
57
|
+
return inspectNestedProjects(rootCwd).projects;
|
|
58
|
+
}
|
|
59
|
+
export function inspectNestedProjects(rootCwd) {
|
|
57
60
|
const root = path.resolve(rootCwd);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const discovered = scanNestedBrainclawProjectsDetailed(root);
|
|
62
|
+
return { projects: Array.from(new Set(discovered.projects
|
|
63
|
+
.map((c) => path.resolve(c.path))
|
|
64
|
+
.filter((abs) => abs !== root && isStrictlyUnder(abs, root)))).sort(), truncated: discovered.truncated };
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* Refresh the whole multi-project workspace: every nested brainclaw project +
|
|
@@ -68,7 +72,8 @@ export async function refreshWorkspaceCascade(input) {
|
|
|
68
72
|
const rootCwd = path.resolve(input.rootCwd);
|
|
69
73
|
// Enumerate nested brainclaw projects strictly under the root (FS scan, so it
|
|
70
74
|
// is strategy-agnostic). De-dup + sort by path for deterministic output.
|
|
71
|
-
const
|
|
75
|
+
const discovery = inspectNestedProjects(rootCwd);
|
|
76
|
+
const childAbsPaths = discovery.projects;
|
|
72
77
|
// Every project to refresh, root first.
|
|
73
78
|
const allProjects = [rootCwd, ...childAbsPaths];
|
|
74
79
|
const refreshOne = async (projectCwd, isRoot) => {
|
|
@@ -77,40 +82,72 @@ export async function refreshWorkspaceCascade(input) {
|
|
|
77
82
|
const nestedUnder = allProjects.filter((p) => p !== projectCwd && isStrictlyUnder(p, projectCwd));
|
|
78
83
|
const extraIgnorePatterns = nestedUnder.map((p) => `${toPosix(path.relative(projectCwd, p))}/**`);
|
|
79
84
|
const projectId = projectIdFor(projectCwd);
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
const projectPath = isRoot ? '.' : toPosix(path.relative(rootCwd, projectCwd));
|
|
86
|
+
try {
|
|
87
|
+
const result = await runRefresh({
|
|
88
|
+
projectId,
|
|
89
|
+
projectRoot: projectCwd,
|
|
90
|
+
scope: input.scope,
|
|
91
|
+
cwd: projectCwd,
|
|
92
|
+
extraIgnorePatterns,
|
|
93
|
+
ownerAgent: input.ownerAgent ?? null,
|
|
94
|
+
ownerAgentId: input.ownerAgentId ?? null,
|
|
95
|
+
});
|
|
96
|
+
const filesIndexed = readManifest(projectCwd)?.stats.files_indexed ?? 0;
|
|
97
|
+
const outcome = !result.lock_acquired
|
|
98
|
+
? 'locked'
|
|
99
|
+
: filesIndexed === 0 ? 'no_eligible_files' : 'indexed';
|
|
100
|
+
return {
|
|
101
|
+
path: projectPath,
|
|
102
|
+
project_id: projectId,
|
|
103
|
+
is_root: isRoot,
|
|
104
|
+
ran: result.ran,
|
|
105
|
+
lock_acquired: result.lock_acquired,
|
|
106
|
+
files_parsed: result.files_parsed,
|
|
107
|
+
files_compacted: result.files_compacted,
|
|
108
|
+
files_indexed: filesIndexed,
|
|
109
|
+
freshness: result.freshness.status,
|
|
110
|
+
outcome,
|
|
111
|
+
...(outcome === 'no_eligible_files' ? { reason: 'no eligible source files found' } : {}),
|
|
112
|
+
...(result.lock_status ? { lock_status: result.lock_status, reason: result.lock_status } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
path: projectPath,
|
|
118
|
+
project_id: projectId,
|
|
119
|
+
is_root: isRoot,
|
|
120
|
+
ran: false,
|
|
121
|
+
lock_acquired: false,
|
|
122
|
+
files_parsed: 0,
|
|
123
|
+
files_compacted: 0,
|
|
124
|
+
files_indexed: readManifest(projectCwd)?.stats.files_indexed ?? null,
|
|
125
|
+
freshness: 'partial',
|
|
126
|
+
outcome: 'failed',
|
|
127
|
+
reason: 'refresh_failed',
|
|
128
|
+
error: error instanceof Error ? error.message : String(error),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
100
131
|
};
|
|
101
132
|
// Children first, then the root (sequential — each holds its own project lock
|
|
102
133
|
// briefly; never blocks bclaw_work, rule 8).
|
|
103
134
|
const children = [];
|
|
135
|
+
const total = childAbsPaths.length + 1;
|
|
136
|
+
input.onProgress?.({ completed: 0, total, current_project: childAbsPaths[0] ? toPosix(path.relative(rootCwd, childAbsPaths[0])) : '.' });
|
|
104
137
|
for (const childCwd of childAbsPaths) {
|
|
105
|
-
|
|
138
|
+
const result = await refreshOne(childCwd, false);
|
|
139
|
+
children.push(result);
|
|
140
|
+
input.onProgress?.({ completed: children.length, total, current_project: children.length < childAbsPaths.length ? toPosix(path.relative(rootCwd, childAbsPaths[children.length])) : '.', last_result: result });
|
|
106
141
|
}
|
|
107
142
|
const rootResult = await refreshOne(rootCwd, true);
|
|
143
|
+
input.onProgress?.({ completed: total, total, current_project: null, last_result: rootResult });
|
|
108
144
|
return {
|
|
109
145
|
is_cascade: true,
|
|
110
146
|
root: rootCwd,
|
|
111
147
|
root_result: rootResult,
|
|
112
148
|
children,
|
|
113
149
|
children_refreshed: children.length,
|
|
150
|
+
discovery_truncated: discovery.truncated,
|
|
114
151
|
};
|
|
115
152
|
}
|
|
116
153
|
//# sourceMappingURL=cascade.js.map
|
|
@@ -232,7 +232,7 @@ export function isTestPath(p) {
|
|
|
232
232
|
/**
|
|
233
233
|
* Score a symbol index entry against the query. Matching is separator/case
|
|
234
234
|
* INSENSITIVE (pln#601): an exact NORMALIZED match scores highest, then prefix,
|
|
235
|
-
* then substring
|
|
235
|
+
* then substring; sub-token-only candidates score zero. Exported symbols + components/hooks
|
|
236
236
|
* get a small boost; test-file symbols are biased DOWN so a test helper never
|
|
237
237
|
* outranks the real definition of the same name (the Fable-audit brief-noise
|
|
238
238
|
* companion to the find defect). Exported for focused ranking tests.
|
|
@@ -250,7 +250,7 @@ export function scoreEntry(entry, query) {
|
|
|
250
250
|
else if (name.includes(q))
|
|
251
251
|
score += 3; // substring
|
|
252
252
|
else
|
|
253
|
-
|
|
253
|
+
return 0; // shared-token noise is not a match
|
|
254
254
|
score *= entry.score_hint; // exported (1.0) vs internal (0.8)
|
|
255
255
|
if (entry.subtype === 'component' || entry.subtype === 'hook')
|
|
256
256
|
score += 1;
|
|
@@ -367,6 +367,9 @@ export function findInStore(query, ctx, checker, acc) {
|
|
|
367
367
|
const confident = validateEntry(entry, checker, acc, root, maxBytes, ctx.cwd, ctx.preferredDirName);
|
|
368
368
|
if (!confident)
|
|
369
369
|
continue;
|
|
370
|
+
const score = scoreEntry(entry, query);
|
|
371
|
+
if (score <= 0)
|
|
372
|
+
continue;
|
|
370
373
|
ranked.push({
|
|
371
374
|
match: {
|
|
372
375
|
node_id: entry.node_id,
|
|
@@ -375,7 +378,7 @@ export function findInStore(query, ctx, checker, acc) {
|
|
|
375
378
|
file_id: entry.file_id,
|
|
376
379
|
kind: entry.kind,
|
|
377
380
|
subtype: entry.subtype ?? null,
|
|
378
|
-
score
|
|
381
|
+
score,
|
|
379
382
|
},
|
|
380
383
|
centrality: importCentrality(entry, resolutionIndex),
|
|
381
384
|
});
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { loadState, mutateState } from './state.js';
|
|
18
|
+
import { detectDuplicates } from './duplicates.js';
|
|
18
19
|
import { archiveCandidate, listCandidates, loadCandidate, saveCandidate, } from './candidates.js';
|
|
19
20
|
import { addCrossProjectLink, removeCrossProjectLink, resolveCrossProjectLinks, } from './cross-project.js';
|
|
20
21
|
import { findActiveClaimsForPlan, listClaims, loadClaim, logCascadeReleaseResult, markClaimStale, releaseClaimsCascade, releaseClaimWithCascade, saveClaim, } from './claims.js';
|
|
@@ -511,6 +512,19 @@ export function getEntity(name, idOrShortLabel, cwd) {
|
|
|
511
512
|
// ─── CREATE ────────────────────────────────────────────────────────────
|
|
512
513
|
export function createEntity(name, data, cwd) {
|
|
513
514
|
assertKnownEntity(name, 'create');
|
|
515
|
+
const proximityKinds = new Set(['decision', 'constraint', 'trap']);
|
|
516
|
+
const nearby = proximityKinds.has(name) && typeof data.text === 'string'
|
|
517
|
+
? detectDuplicates(data.text, name, loadState(cwd), listCandidates(undefined, cwd).filter((candidate) => candidate.status === 'pending')).slice(0, 3).map((match) => ({
|
|
518
|
+
id: match.id,
|
|
519
|
+
source: match.source,
|
|
520
|
+
reason: match.reason,
|
|
521
|
+
preview: match.text.length > 160 ? `${match.text.slice(0, 157)}…` : match.text,
|
|
522
|
+
}))
|
|
523
|
+
: [];
|
|
524
|
+
const result = (base) => ({
|
|
525
|
+
...base,
|
|
526
|
+
...(nearby.length ? { nearby_items: nearby } : {}),
|
|
527
|
+
});
|
|
514
528
|
switch (name) {
|
|
515
529
|
case 'plan': {
|
|
516
530
|
// Explicit field whitelist + required-author check brings plan create in line
|
|
@@ -530,7 +544,7 @@ export function createEntity(name, data, cwd) {
|
|
|
530
544
|
estimatedEffort: data.estimated_effort,
|
|
531
545
|
}, cwd);
|
|
532
546
|
stampProvenanceOnStateItem('plan', res.id, defaultProvenance(data), cwd);
|
|
533
|
-
return { entity: name, id: res.id, short_label: res.shortLabel };
|
|
547
|
+
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
534
548
|
}
|
|
535
549
|
case 'decision': {
|
|
536
550
|
const res = createDecision({
|
|
@@ -542,7 +556,7 @@ export function createEntity(name, data, cwd) {
|
|
|
542
556
|
planId: data.plan_id,
|
|
543
557
|
}, cwd);
|
|
544
558
|
stampProvenanceOnStateItem('decision', res.id, defaultProvenance(data), cwd);
|
|
545
|
-
return { entity: name, id: res.id, short_label: res.shortLabel };
|
|
559
|
+
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
546
560
|
}
|
|
547
561
|
case 'constraint': {
|
|
548
562
|
const res = createConstraint({
|
|
@@ -553,7 +567,7 @@ export function createEntity(name, data, cwd) {
|
|
|
553
567
|
relatedPaths: data.related_paths,
|
|
554
568
|
}, cwd);
|
|
555
569
|
stampProvenanceOnStateItem('constraint', res.id, defaultProvenance(data), cwd);
|
|
556
|
-
return { entity: name, id: res.id, short_label: res.shortLabel };
|
|
570
|
+
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
557
571
|
}
|
|
558
572
|
case 'trap': {
|
|
559
573
|
const res = createTrap({
|
|
@@ -564,7 +578,7 @@ export function createEntity(name, data, cwd) {
|
|
|
564
578
|
relatedPaths: data.related_paths,
|
|
565
579
|
}, cwd);
|
|
566
580
|
stampProvenanceOnStateItem('trap', res.id, defaultProvenance(data), cwd);
|
|
567
|
-
return { entity: name, id: res.id, short_label: res.shortLabel };
|
|
581
|
+
return result({ entity: name, id: res.id, short_label: res.shortLabel });
|
|
568
582
|
}
|
|
569
583
|
case 'runtime_note': {
|
|
570
584
|
const id = generateId('runtime_note');
|
|
@@ -213,6 +213,12 @@ export function withCodexWorkspaceRoot(invoke, agent, worktreePath, isWin32 = pr
|
|
|
213
213
|
}
|
|
214
214
|
return { ...invoke, args, bashCommand };
|
|
215
215
|
}
|
|
216
|
+
/** Refuse a stdin-delivery invoke that would otherwise inherit `/dev/null`. */
|
|
217
|
+
export function assertPromptDelivery(invoke) {
|
|
218
|
+
if (invoke.promptDelivery === 'stdin_pipe' && !invoke.promptText?.trim()) {
|
|
219
|
+
throw new Error('Invalid stdin_pipe invocation: promptText is empty; refusing to spawn an agent with ignored stdin.');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
216
222
|
export class CliExecutionAdapter {
|
|
217
223
|
id = 'cli';
|
|
218
224
|
canSpawn(agentName) {
|
|
@@ -281,6 +287,7 @@ export class CliExecutionAdapter {
|
|
|
281
287
|
};
|
|
282
288
|
}
|
|
283
289
|
start(invoke, options) {
|
|
290
|
+
assertPromptDelivery(invoke);
|
|
284
291
|
const isWin32 = process.platform === 'win32';
|
|
285
292
|
invoke = withCodexWorkspaceRoot(invoke, options.agent, options.worktreePath, isWin32);
|
|
286
293
|
// F7 (trp_0e5150d3): route worker env through buildWorkerIdentityEnv so the
|
|
@@ -317,7 +324,7 @@ export class CliExecutionAdapter {
|
|
|
317
324
|
}
|
|
318
325
|
const spawnExecutable = resolvedExecutable ?? invoke.executable;
|
|
319
326
|
const useShell = isWin32 && /\.(cmd|bat)$/i.test(spawnExecutable);
|
|
320
|
-
const needsStdin = invoke.promptDelivery === 'stdin_pipe'
|
|
327
|
+
const needsStdin = invoke.promptDelivery === 'stdin_pipe';
|
|
321
328
|
// pln#520 step 4: when we ack-wrap, the SHELL redirects stdout/stderr to the
|
|
322
329
|
// per-assignment log files (fds passed via stdio are NOT inherited through
|
|
323
330
|
// the cmd.exe → .cmd → node shim — the empty-logs bug of can_f792cacd), and
|
package/dist/core/spawn-check.js
CHANGED
|
@@ -96,7 +96,15 @@ export async function checkAgentSpawn(agent, options = {}) {
|
|
|
96
96
|
const stderrRaw = readLogTail(root, assignmentId, 'stderr', 800).trim();
|
|
97
97
|
const stderrTail = stderrRaw ? stderrRaw.split(/\r?\n/).filter(Boolean) : undefined;
|
|
98
98
|
if (completed) {
|
|
99
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
agent,
|
|
101
|
+
binary,
|
|
102
|
+
status: 'ok',
|
|
103
|
+
delivered,
|
|
104
|
+
completed: true,
|
|
105
|
+
duration_ms,
|
|
106
|
+
detail: `validation probe: ack + completed round-trip (${invoke.promptDelivery}, ${invoke.promptText?.length ?? 0} prompt bytes)`,
|
|
107
|
+
};
|
|
100
108
|
}
|
|
101
109
|
if (failed) {
|
|
102
110
|
const tail = stderrRaw || readLogTail(root, assignmentId, 'stdout', 400).trim();
|
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.28.
|
|
2
|
+
// Source: brainclaw v1.28.3 on 2026-08-25T23:07:33.877Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.28.
|
|
5
|
-
"generated_at": "2026-08-
|
|
4
|
+
"version": "1.28.3",
|
|
5
|
+
"generated_at": "2026-08-25T23:07:33.877Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 70,
|
|
8
8
|
"published_count": 68,
|
|
@@ -478,7 +478,7 @@ export const FACTS = {
|
|
|
478
478
|
},
|
|
479
479
|
"bench": {
|
|
480
480
|
"schema": "brainclaw.bench.v1",
|
|
481
|
-
"generated_at": "2026-08-
|
|
481
|
+
"generated_at": "2026-08-25T23:07:31.792Z",
|
|
482
482
|
"node_version": "v24.19.0",
|
|
483
483
|
"platform": "linux-x64",
|
|
484
484
|
"repeats": 3,
|
|
@@ -487,7 +487,7 @@ export const FACTS = {
|
|
|
487
487
|
"name": "cold_onboard",
|
|
488
488
|
"volume": "empty",
|
|
489
489
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
490
|
-
"duration_ms_median":
|
|
490
|
+
"duration_ms_median": 79,
|
|
491
491
|
"payload_chars_median": 1640,
|
|
492
492
|
"payload_tokens_est_median": 410
|
|
493
493
|
},
|
|
@@ -495,7 +495,7 @@ export const FACTS = {
|
|
|
495
495
|
"name": "warm_work",
|
|
496
496
|
"volume": "medium",
|
|
497
497
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
498
|
-
"duration_ms_median":
|
|
498
|
+
"duration_ms_median": 122,
|
|
499
499
|
"payload_chars_median": 2626,
|
|
500
500
|
"payload_tokens_est_median": 657
|
|
501
501
|
},
|
|
@@ -503,7 +503,7 @@ export const FACTS = {
|
|
|
503
503
|
"name": "first_edit",
|
|
504
504
|
"volume": "medium",
|
|
505
505
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
506
|
-
"duration_ms_median":
|
|
506
|
+
"duration_ms_median": 11,
|
|
507
507
|
"payload_chars_median": 1629,
|
|
508
508
|
"payload_tokens_est_median": 407
|
|
509
509
|
}
|
package/dist/facts.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.28.
|
|
3
|
-
"generated_at": "2026-08-
|
|
2
|
+
"version": "1.28.3",
|
|
3
|
+
"generated_at": "2026-08-25T23:07:33.877Z",
|
|
4
4
|
"tools": {
|
|
5
5
|
"count": 70,
|
|
6
6
|
"published_count": 68,
|
|
@@ -476,7 +476,7 @@
|
|
|
476
476
|
},
|
|
477
477
|
"bench": {
|
|
478
478
|
"schema": "brainclaw.bench.v1",
|
|
479
|
-
"generated_at": "2026-08-
|
|
479
|
+
"generated_at": "2026-08-25T23:07:31.792Z",
|
|
480
480
|
"node_version": "v24.19.0",
|
|
481
481
|
"platform": "linux-x64",
|
|
482
482
|
"repeats": 3,
|
|
@@ -485,7 +485,7 @@
|
|
|
485
485
|
"name": "cold_onboard",
|
|
486
486
|
"volume": "empty",
|
|
487
487
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
488
|
-
"duration_ms_median":
|
|
488
|
+
"duration_ms_median": 79,
|
|
489
489
|
"payload_chars_median": 1640,
|
|
490
490
|
"payload_tokens_est_median": 410
|
|
491
491
|
},
|
|
@@ -493,7 +493,7 @@
|
|
|
493
493
|
"name": "warm_work",
|
|
494
494
|
"volume": "medium",
|
|
495
495
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
496
|
-
"duration_ms_median":
|
|
496
|
+
"duration_ms_median": 122,
|
|
497
497
|
"payload_chars_median": 2626,
|
|
498
498
|
"payload_tokens_est_median": 657
|
|
499
499
|
},
|
|
@@ -501,7 +501,7 @@
|
|
|
501
501
|
"name": "first_edit",
|
|
502
502
|
"volume": "medium",
|
|
503
503
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
504
|
-
"duration_ms_median":
|
|
504
|
+
"duration_ms_median": 11,
|
|
505
505
|
"payload_chars_median": 1629,
|
|
506
506
|
"payload_tokens_est_median": 407
|
|
507
507
|
}
|
package/docs/cli.md
CHANGED
|
@@ -639,11 +639,11 @@ Full reference (freshness model, supported languages, WASM bundling): [docs/code
|
|
|
639
639
|
|
|
640
640
|
### `brainclaw code-map status [--cascade]`
|
|
641
641
|
|
|
642
|
-
Store presence, freshness badge (`fresh` / `stale_changed_files` / `stale_extractor` / `stale_grammar` / `partial` / `missing_index`), and index stats (files, nodes, edges). Read-only. In a multi-project workspace, `--cascade` adds
|
|
642
|
+
Store presence, freshness badge (`fresh` / `stale_changed_files` / `stale_extractor` / `stale_grammar` / `partial` / `missing_index`), and index stats (files, nodes, edges). Read-only. In a multi-project workspace, `--cascade` adds compact coverage counts and names only non-fresh projects. On the MCP surface, the equivalent `bclaw_code_status(cascade=true)` also follows the latest durable cascade job.
|
|
643
643
|
|
|
644
644
|
### `brainclaw code-map refresh [--all|--changed] [--cascade]`
|
|
645
645
|
|
|
646
|
-
Build or update the index. `--changed` (default) re-parses only touched files; `--all` does a full re-index. Run this when status shows `missing_index` or a stale badge. Fails fast (never blocks) if another writer holds the project lock. In a multi-project workspace, `--cascade` refreshes
|
|
646
|
+
Build or update the index. `--changed` (default) re-parses only touched files; `--all` does a full re-index. Run this when status shows `missing_index` or a stale badge. Fails fast (never blocks) if another writer holds the project lock. In a multi-project workspace, `--cascade` synchronously refreshes each discovered project into its own store plus a root store scoped to files no child owns (zero double-indexing). The MCP equivalent starts a durable background job instead; follow it with `bclaw_code_status(cascade=true)`. See [docs/code-map.md](code-map.md#cascading-a-multi-project-workspace---cascade).
|
|
647
647
|
|
|
648
648
|
### `brainclaw code-map find <query> [--limit <n>]`
|
|
649
649
|
|
package/docs/code-map.md
CHANGED
|
@@ -126,11 +126,11 @@ all return a `freshness_badge`:
|
|
|
126
126
|
|
|
127
127
|
| Tool | Kind | Purpose |
|
|
128
128
|
|---|---|---|
|
|
129
|
-
| `bclaw_code_status` | read |
|
|
129
|
+
| `bclaw_code_status` | read | Active-session project store, freshness, index stats; `cascade=true` also follows the latest cascade job. Never refreshes. |
|
|
130
130
|
| `bclaw_code_find` | read | Ranked symbol-index search (`query`, optional `limit`). Never refreshes. |
|
|
131
131
|
| `bclaw_code_brief` | read | Reading brief for a symbol/path (`target`, optional `limit`, files capped at 12). Never refreshes. |
|
|
132
132
|
| `bclaw_code_export` | read | Bounded local subgraph around required `target`; direction/depth/node/edge caps, confidence filtering, and optional Mermaid projection. Never refreshes. |
|
|
133
|
-
| `bclaw_code_refresh` | write | Rebuild the index. `scope` = `"changed"` (default) or `"all"
|
|
133
|
+
| `bclaw_code_refresh` | write | Rebuild the index. `scope` = `"changed"` (default) or `"all"`; MCP `cascade=true` starts a durable background job and returns immediately. |
|
|
134
134
|
|
|
135
135
|
The read tools never trigger a parse — if `bclaw_code_status` /
|
|
136
136
|
`bclaw_code_find` / `bclaw_code_brief` report `missing_index` or a stale badge,
|
|
@@ -178,7 +178,7 @@ No read command parses files or refreshes the index. `bclaw_work` can suggest
|
|
|
178
178
|
that explicit refresh, but never performs it lazily.
|
|
179
179
|
## Lifecycle — pull-based, no daemon
|
|
180
180
|
|
|
181
|
-
Code Map never
|
|
181
|
+
Code Map never auto-reindexes and has no daemon. The model is lazy
|
|
182
182
|
reconciliation at the read path:
|
|
183
183
|
|
|
184
184
|
1. You edit or pull code — the index does not change.
|
|
@@ -186,7 +186,9 @@ reconciliation at the read path:
|
|
|
186
186
|
file-hash diff vs the stored shards), so a stale index is always *visible*,
|
|
187
187
|
never silently wrong.
|
|
188
188
|
3. `refresh --changed` re-parses only the changed files (incremental); `--all` does
|
|
189
|
-
a full rebuild + orphan compaction.
|
|
189
|
+
a full rebuild + orphan compaction. The one bounded background path is an
|
|
190
|
+
explicitly requested MCP monorepo cascade, whose durable progress is read
|
|
191
|
+
through `bclaw_code_status(cascade=true)`.
|
|
190
192
|
4. `bclaw_work` nudges a refresh when the badge is `missing_index` or stale, so an
|
|
191
193
|
agent knows to reconcile before trusting the map.
|
|
192
194
|
|
|
@@ -240,9 +242,15 @@ double-indexing**, even when projects nest inside one another. `--cascade` is
|
|
|
240
242
|
opt-in; without it, the root refresh keeps its single-tree behaviour (above), and
|
|
241
243
|
single-project repos ignore the flag entirely.
|
|
242
244
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
The CLI cascade stays synchronous. MCP `bclaw_code_refresh(cascade=true)` returns
|
|
246
|
+
a durable `job_id` immediately, avoiding the client timeout that a large workspace
|
|
247
|
+
can hit; follow it with `bclaw_code_status(cascade=true)`. Status reports completed
|
|
248
|
+
and total project counts, the project currently being indexed, and terminal
|
|
249
|
+
outcomes. Successful rows are aggregated; only exceptions are named. A project
|
|
250
|
+
with a valid empty index is labeled `no_eligible_files`, while lock contention and
|
|
251
|
+
refresh failures remain distinct (`locked` / `failed`). `discovery_truncated=true`
|
|
252
|
+
warns that the bounded nested-project scan could not inspect deeper branches, so
|
|
253
|
+
the reported project total must not be treated as complete.
|
|
246
254
|
|
|
247
255
|
### Workspace-wide `find` / `brief`
|
|
248
256
|
|
|
@@ -250,8 +258,11 @@ Once the per-child indexes exist (built by `--cascade`), `find` and `brief` run
|
|
|
250
258
|
at a multi-project workspace **root** automatically aggregate across every child
|
|
251
259
|
project's store — no flag needed. Matches are project-tagged with
|
|
252
260
|
workspace-relative paths, and the freshness badge merges per-store status (worst
|
|
253
|
-
status wins) plus coverage
|
|
254
|
-
|
|
261
|
+
status wins) plus coverage. Missing child stores make the top-line badge
|
|
262
|
+
`partial`, never `fresh`; diagnostics carry status counts and only the non-fresh
|
|
263
|
+
exceptions instead of repeating every project. Weak shared-token candidates that
|
|
264
|
+
do not contain the normalized query are omitted rather than returned as plausible
|
|
265
|
+
score-1/2 noise. An aggregated `brief` also surfaces **cross-package reverse
|
|
255
266
|
dependents**: sibling packages that import the defining package's public name
|
|
256
267
|
rank into the reading list, flagged `cross_package`.
|
|
257
268
|
|
package/docs/integrations/mcp.md
CHANGED
|
@@ -108,13 +108,13 @@ Each tool also has an `annotations.category` field: `session`, `context`, `memor
|
|
|
108
108
|
| `bclaw_remove` | memory | Archive or purge a canonical entity |
|
|
109
109
|
| `bclaw_transition` | memory | Move an entity through its validated state machine |
|
|
110
110
|
| `bclaw_move` | memory | Relocate an item to another project, id-preserving (multi-project) |
|
|
111
|
-
| `bclaw_code_status` | discovery | Code Map freshness
|
|
111
|
+
| `bclaw_code_status` | discovery | Active-session Code Map freshness + stats; `cascade:true` follows durable monorepo refresh progress and exceptions |
|
|
112
112
|
| `bclaw_code_find` | discovery | Search the Code Map symbol index by name (function/class/component/hook/type) |
|
|
113
113
|
| `bclaw_code_brief` | discovery | Ranked reading list + related decisions/traps before editing a symbol or path |
|
|
114
114
|
| `bclaw_code_impact` | discovery | Explainable local blast radius from resolved imports: definition, direct dependents, opt-in bounded transitives, tests, and count-based risk |
|
|
115
115
|
| `bclaw_code_export` | discovery | Compact bounded local nodes/edges around one symbol or file; preserves edge kind/source/confidence, with optional Mermaid projection |
|
|
116
116
|
| `bclaw_code_outline` | discovery | Source-ordered symbols of one indexed file (span, exported, confidence) — no reparse |
|
|
117
|
-
| `bclaw_code_refresh` | discovery | Rebuild the Code Map index (`scope: changed \| all`) |
|
|
117
|
+
| `bclaw_code_refresh` | discovery | Rebuild the Code Map index (`scope: changed \| all`); `cascade:true` starts a durable background job |
|
|
118
118
|
|
|
119
119
|
See [code map](../code-map.md) for the full Code Map reference (CLI, freshness model, supported languages).
|
|
120
120
|
|
|
@@ -8,6 +8,36 @@ guarantees this changelog follows.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [1.28.3] — 2026-08-26
|
|
12
|
+
|
|
13
|
+
**Changed — durable Code Map cascade execution**
|
|
14
|
+
|
|
15
|
+
- `bclaw_code_refresh({ cascade: true })` now returns a durable `job_id`
|
|
16
|
+
immediately for multi-project workspaces instead of keeping the MCP request
|
|
17
|
+
open for the whole synchronous cascade.
|
|
18
|
+
- `bclaw_code_status({ cascade: true })` adds the latest job's lifecycle and
|
|
19
|
+
progress (`queued | running | completed | failed`, project counts and current
|
|
20
|
+
project), then a bounded terminal summary with outcome counts and problem
|
|
21
|
+
projects. Discovery truncation and `no_eligible_files`, `locked`, and `failed`
|
|
22
|
+
outcomes remain explicit.
|
|
23
|
+
- All Code Map MCP tools now resolve against the active session project selected
|
|
24
|
+
by `bclaw_work` / `bclaw_switch`; this corrects routing behavior without
|
|
25
|
+
changing their input schemas.
|
|
26
|
+
|
|
27
|
+
**Added — non-blocking proximity hints on canonical memory creation**
|
|
28
|
+
|
|
29
|
+
- Successful `bclaw_create` calls for decisions, constraints, and traps may add
|
|
30
|
+
`nearby_items` (at most three bounded previews with ids and match reasons).
|
|
31
|
+
The requested creation is never rejected solely because a nearby item exists.
|
|
32
|
+
|
|
33
|
+
**Changed — admission failures become pre-mutation**
|
|
34
|
+
|
|
35
|
+
- Unsupported true cross-project auto-execution and empty `stdin_pipe` prompt
|
|
36
|
+
delivery now fail before claims, assignments, loops, or worker processes are
|
|
37
|
+
created. Existing successful response shapes and input schemas are unchanged.
|
|
38
|
+
|
|
39
|
+
No tool was added, removed, or renamed in this release.
|
|
40
|
+
|
|
11
41
|
## [1.20.3] — 2026-08-03
|
|
12
42
|
|
|
13
43
|
**Changed — `bclaw_dispatch_status` diagnosis values under the fs-activity veto (#170)**
|