brainclaw 1.28.3 → 1.28.5
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 +6 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-code-map.js +1 -0
- package/dist/commands/code-map.js +5 -1
- package/dist/commands/harvest.js +67 -25
- package/dist/commands/loops-handlers.js +28 -1
- package/dist/commands/mcp-catalog.js +32 -4
- package/dist/commands/mcp-read-handlers.js +15 -1
- package/dist/commands/mcp-schemas.generated.js +13 -0
- package/dist/commands/mcp-write-coordination.js +84 -22
- package/dist/commands/mcp-write-memory.js +87 -1
- package/dist/commands/mcp.js +60 -11
- package/dist/commands/switch.js +8 -1
- package/dist/core/code-map/backend.js +19 -5
- package/dist/core/code-map/refresh-jobs.js +158 -0
- package/dist/core/code-map/refresh-worker.js +12 -0
- package/dist/core/context.js +16 -3
- package/dist/core/dispatch-status.js +36 -14
- package/dist/core/dispatcher.js +28 -20
- package/dist/core/entity-operations.js +62 -4
- package/dist/core/entity-registry.js +3 -3
- package/dist/core/execution-adapters.js +10 -0
- package/dist/core/facade-schema.js +10 -0
- package/dist/core/ideation-loop-close.js +3 -1
- package/dist/core/lane-result-file.js +72 -0
- package/dist/core/loop-turn-dispatch.js +2 -0
- package/dist/core/loops/brief-assembly.js +19 -11
- package/dist/core/loops/next-expected.js +56 -1
- package/dist/core/loops/reconcile-turn.js +8 -0
- package/dist/core/loops/result-reducers.js +14 -12
- package/dist/core/loops/store.js +4 -0
- package/dist/core/loops/types.js +14 -2
- package/dist/core/loops/verbs.js +8 -1
- package/dist/core/loops/worker-reply-contract.js +1 -1
- package/dist/core/protocol-tool-policy.js +1 -0
- package/dist/core/review-loop-turn-dispatch.js +1 -0
- package/dist/core/schema.js +24 -1
- package/dist/core/search.js +3 -2
- package/dist/core/worktree.js +14 -7
- package/dist/facts.js +14 -13
- package/dist/facts.json +13 -12
- package/docs/cli.md +37 -4
- package/docs/code-map.md +30 -18
- package/docs/concepts/ideation-loop.md +35 -14
- package/docs/integrations/mcp.md +17 -7
- package/docs/mcp-schema-changelog.md +63 -6
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
import crypto from 'node:crypto';
|
|
15
|
+
import fs from 'node:fs';
|
|
15
16
|
import path from 'node:path';
|
|
16
17
|
import { spawnSync } from 'node:child_process';
|
|
17
18
|
import { buildClaimEnvPrefix } from '../core/execution-profile.js';
|
|
@@ -22,8 +23,7 @@ import { appendAuditEntry } from '../core/audit.js';
|
|
|
22
23
|
import { nowISO } from '../core/ids.js';
|
|
23
24
|
import { validateMcpField } from '../core/input-validation.js';
|
|
24
25
|
import { generateCandidateIdWithLabel, saveCandidate } from '../core/candidates.js';
|
|
25
|
-
import { DEFAULT_PROTOCOLS } from '../core/loops/types.js';
|
|
26
|
-
import { capLoopArtifactBody } from '../core/loops/result-reducers.js';
|
|
26
|
+
import { DEFAULT_PROTOCOLS, LOOP_PROPOSAL_BODY_MAX_BYTES } from '../core/loops/types.js';
|
|
27
27
|
import { validateLoopProjectResolution } from '../core/loops/project-resolution.js';
|
|
28
28
|
import { coordinateNextActions, dispatchNextActions } from '../core/next-actions.js';
|
|
29
29
|
import { agentValidationFailedWarning, consultAutoExecuteNoOpWarning, planAlreadyAssignedWarning, pushStructuredWarning, scopeAlreadyClaimedWarning, } from '../core/warnings.js';
|
|
@@ -309,6 +309,25 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
309
309
|
return { response: createToolErrorResponse('validation_error', parseResult.error.message) };
|
|
310
310
|
}
|
|
311
311
|
const req = parseResult.data;
|
|
312
|
+
// A proposal is the caller's task contract. Silently replacing its tail with
|
|
313
|
+
// memory changed the questions workers answered during DGX dogfooding. Keep
|
|
314
|
+
// the whole task or reject before mutation; never truncate it in-band.
|
|
315
|
+
if (req.intent === 'ideate') {
|
|
316
|
+
const taskBytes = Buffer.byteLength(req.task, 'utf8');
|
|
317
|
+
if (taskBytes > LOOP_PROPOSAL_BODY_MAX_BYTES) {
|
|
318
|
+
return {
|
|
319
|
+
response: createToolErrorResponse('ideate_task_too_large', `ideation task is ${taskBytes} bytes; the lossless limit is ${LOOP_PROPOSAL_BODY_MAX_BYTES} bytes. Shorten it or attach a referenced artifact before retrying; no loop was created.`, { task_bytes: taskBytes, task_limit_bytes: LOOP_PROPOSAL_BODY_MAX_BYTES, task_truncated: false }),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (req.criticPerspectives) {
|
|
323
|
+
const targetCount = req.targetAgents?.length ?? 0;
|
|
324
|
+
if (targetCount === 0 || req.criticPerspectives.length !== targetCount) {
|
|
325
|
+
return {
|
|
326
|
+
response: createToolErrorResponse('ideate_perspective_count_mismatch', `criticPerspectives must contain exactly one instruction per targetAgents entry (targets=${targetCount}, perspectives=${req.criticPerspectives.length}); no loop was created.`),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
312
331
|
// pln#511 step 2 — preset selector validation. Presets are kind-
|
|
313
332
|
// specific in v1: only intent='ideate' carries them. Unknown names
|
|
314
333
|
// are rejected up-front against the registry so the handler never
|
|
@@ -427,8 +446,9 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
427
446
|
// pln#692 P0 — admission must prove that a multi-agent ideation request can
|
|
428
447
|
// satisfy the first worker-produced phase gate BEFORE openLoop (or any
|
|
429
448
|
// identity/claim/assignment mutation). The default critique phase requires
|
|
430
|
-
// three distinct critique artifacts
|
|
431
|
-
//
|
|
449
|
+
// three distinct critique artifacts. Capacity is therefore counted per
|
|
450
|
+
// requested critic INSTANCE, not per unique agent identity: each occurrence
|
|
451
|
+
// becomes an isolated slot with its own claim, worktree and turn authority.
|
|
432
452
|
if (req.intent === 'ideate'
|
|
433
453
|
&& req.preset !== 'bootstrap'
|
|
434
454
|
&& Array.isArray(req.targetAgents)
|
|
@@ -436,23 +456,27 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
436
456
|
const critiquePhase = DEFAULT_PROTOCOLS.ideation.phases.find((phase) => phase.name === 'critique');
|
|
437
457
|
const gate = critiquePhase?.advance_gate;
|
|
438
458
|
const requiredCritics = gate?.kind === 'min_artifacts_by_type' ? gate.n : 0;
|
|
439
|
-
const
|
|
440
|
-
const checks = uniqueTargets.map((agent) => ({
|
|
459
|
+
const checks = req.targetAgents.map((agent, instanceIndex) => ({
|
|
441
460
|
agent,
|
|
461
|
+
instanceIndex,
|
|
442
462
|
check: validateAgentForDispatch(agent, { requireSpawnable: true }),
|
|
443
463
|
}));
|
|
444
464
|
const executableTargets = checks.filter(({ check }) => check.valid).map(({ agent }) => agent);
|
|
445
|
-
const invalidTargets = checks.filter(({ check }) => !check.valid).map(({ agent, check }) => ({
|
|
465
|
+
const invalidTargets = checks.filter(({ check }) => !check.valid).map(({ agent, instanceIndex, check }) => ({
|
|
446
466
|
agent,
|
|
467
|
+
instance_index: instanceIndex,
|
|
447
468
|
code: check.code,
|
|
448
469
|
reason: check.reason,
|
|
449
470
|
}));
|
|
450
471
|
if (requiredCritics > 0 && executableTargets.length < requiredCritics) {
|
|
451
472
|
const availableTargets = getSpawnableAgents()
|
|
452
473
|
.map((profile) => profile.name)
|
|
453
|
-
.filter((agent, index, all) =>
|
|
474
|
+
.filter((agent, index, all) => all.indexOf(agent) === index)
|
|
454
475
|
.filter((agent) => validateAgentForDispatch(agent, { requireSpawnable: true }).valid);
|
|
455
|
-
const
|
|
476
|
+
const recoveryAgent = executableTargets[0] ?? availableTargets[0];
|
|
477
|
+
const recoveryTargets = recoveryAgent
|
|
478
|
+
? Array.from({ length: requiredCritics }, () => recoveryAgent)
|
|
479
|
+
: [];
|
|
456
480
|
const nextActions = recoveryTargets.length >= requiredCritics
|
|
457
481
|
? [{
|
|
458
482
|
tool: 'bclaw_coordinate',
|
|
@@ -460,21 +484,20 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
460
484
|
intent: 'ideate', task: req.task, scope: req.scope,
|
|
461
485
|
targetAgents: recoveryTargets, autoExecute: effectiveAutoExecute !== false,
|
|
462
486
|
},
|
|
463
|
-
when: `retry with at least ${requiredCritics}
|
|
487
|
+
when: `retry with at least ${requiredCritics} executable critic instances`,
|
|
464
488
|
}]
|
|
465
489
|
: [{
|
|
466
490
|
tool: 'bclaw_context',
|
|
467
491
|
args: { kind: 'execution', includeAgentTooling: true },
|
|
468
|
-
when:
|
|
492
|
+
when: 'configure at least one spawnable critic identity before retrying',
|
|
469
493
|
}];
|
|
470
494
|
return {
|
|
471
|
-
response: createToolErrorResponse('ideate_gate_capacity_unavailable', `ideation admission refused before mutation: critique gate requires ${requiredCritics}
|
|
495
|
+
response: createToolErrorResponse('ideate_gate_capacity_unavailable', `ideation admission refused before mutation: critique gate requires ${requiredCritics} executable critic instance(s), observed ${executableTargets.length}`, {
|
|
472
496
|
gate: { phase: 'critique', kind: gate?.kind, expected: requiredCritics, observed: executableTargets.length },
|
|
473
497
|
requested_targets: req.targetAgents,
|
|
474
498
|
executable_targets: executableTargets,
|
|
475
499
|
invalid_targets: invalidTargets,
|
|
476
500
|
blockers: [
|
|
477
|
-
...(uniqueTargets.length < req.targetAgents.length ? ['duplicate target identities do not add executable capacity'] : []),
|
|
478
501
|
...(invalidTargets.length > 0 ? ['one or more requested targets are not spawnable'] : []),
|
|
479
502
|
`missing executable critic capacity: ${requiredCritics - executableTargets.length}`,
|
|
480
503
|
],
|
|
@@ -738,6 +761,18 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
738
761
|
contextEnvelope: options?.contextEnvelope,
|
|
739
762
|
});
|
|
740
763
|
};
|
|
764
|
+
const compactDeliveryEntry = (entry) => {
|
|
765
|
+
if (!entry.command || entry.command.length <= 2048)
|
|
766
|
+
return entry;
|
|
767
|
+
const dir = path.join(dispatchCwd, '.brainclaw', 'coordination', 'runtime', 'manual-commands');
|
|
768
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
769
|
+
const ext = entry.shell === 'cmd' ? 'cmd' : 'sh';
|
|
770
|
+
const ref = entry.assignment_id ?? entry.message_id;
|
|
771
|
+
const commandFile = path.join(dir, `${ref}.${ext}`);
|
|
772
|
+
fs.writeFileSync(commandFile, entry.command, { encoding: 'utf8', mode: 0o600 });
|
|
773
|
+
const { command, ...rest } = entry;
|
|
774
|
+
return { ...rest, command_file: commandFile, command_bytes: Buffer.byteLength(command, 'utf8') };
|
|
775
|
+
};
|
|
741
776
|
const toMessageSummary = (deliveryPlan) => deliveryPlan.map((entry) => ({
|
|
742
777
|
agent: entry.agent,
|
|
743
778
|
message_id: entry.message_id,
|
|
@@ -1846,11 +1881,19 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1846
1881
|
},
|
|
1847
1882
|
];
|
|
1848
1883
|
if (explicitTargets) {
|
|
1849
|
-
|
|
1884
|
+
const defaultPerspectives = [
|
|
1885
|
+
'Challenge assumptions and verify the proposal against concrete evidence.',
|
|
1886
|
+
'Focus on failure modes, operational risks, and recovery paths; challenge earlier contributions explicitly.',
|
|
1887
|
+
'Develop competing alternatives and compare their costs and trade-offs; resolve or sharpen earlier disagreements.',
|
|
1888
|
+
];
|
|
1889
|
+
for (const [index, agent] of req.targetAgents.entries()) {
|
|
1850
1890
|
const criticIdentity = findAgentIdentityByName(agent, dispatchCwd) ?? ensureAgentRegisteredForDispatch(agent, dispatchCwd);
|
|
1851
1891
|
slots.push({
|
|
1852
1892
|
role: 'critic',
|
|
1853
1893
|
agent,
|
|
1894
|
+
perspective: req.criticPerspectives?.[index]
|
|
1895
|
+
?? defaultPerspectives[index]
|
|
1896
|
+
?? `Challenge the conversation from an independent perspective ${index + 1}; avoid repeating prior contributions.`,
|
|
1854
1897
|
...(criticIdentity?.agent_id ? { agent_id: criticIdentity.agent_id } : {}),
|
|
1855
1898
|
});
|
|
1856
1899
|
}
|
|
@@ -1878,7 +1921,12 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1878
1921
|
stop_condition: presetSelected.stop_condition,
|
|
1879
1922
|
protocol: presetSelected.protocol,
|
|
1880
1923
|
}
|
|
1881
|
-
: {
|
|
1924
|
+
: {
|
|
1925
|
+
protocol: {
|
|
1926
|
+
iteration: DEFAULT_PROTOCOLS.ideation.iteration,
|
|
1927
|
+
ideation_schedule: req.ideation_schedule,
|
|
1928
|
+
},
|
|
1929
|
+
}),
|
|
1882
1930
|
}, dispatchCwd);
|
|
1883
1931
|
loopId = loop.id;
|
|
1884
1932
|
artifacts.push({ type: 'loop', id: loop.id });
|
|
@@ -1892,14 +1940,13 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1892
1940
|
// loop doesn't contain. The task text is already captured on
|
|
1893
1941
|
// the thread (title + goal).
|
|
1894
1942
|
if (!presetSelected) {
|
|
1895
|
-
const proposalBody = capLoopArtifactBody(req.task);
|
|
1896
1943
|
const updated = add_artifact({
|
|
1897
1944
|
id: loop.id,
|
|
1898
1945
|
actor: creatorActor,
|
|
1899
1946
|
artifact: {
|
|
1900
1947
|
phase: 'proposal',
|
|
1901
1948
|
type: 'proposal',
|
|
1902
|
-
body:
|
|
1949
|
+
body: req.task,
|
|
1903
1950
|
produced_by: creatorActor,
|
|
1904
1951
|
},
|
|
1905
1952
|
}, dispatchCwd);
|
|
@@ -1918,8 +1965,11 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1918
1965
|
};
|
|
1919
1966
|
}
|
|
1920
1967
|
} // end else (non-bootstrap open path)
|
|
1921
|
-
//
|
|
1922
|
-
//
|
|
1968
|
+
// Multi-agent ideation keeps artifact capacity separate from execution
|
|
1969
|
+
// concurrency. All requested critic slots are durable, but sequential is
|
|
1970
|
+
// the default scheduling policy: only the first open slot is dispatched
|
|
1971
|
+
// now, and the next one is taken after this result is harvested. Explicit
|
|
1972
|
+
// parallel mode retains the historical immediate fan-out.
|
|
1923
1973
|
//
|
|
1924
1974
|
// pln#511 step 2 — initial phase comes from the actual loop's
|
|
1925
1975
|
// first phase, not a hardcoded 'proposal'. Presets like bootstrap
|
|
@@ -1977,7 +2027,10 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1977
2027
|
throw new Error('ideate dispatch: loop disappeared after advance');
|
|
1978
2028
|
}
|
|
1979
2029
|
dispatchedPhase = advancedLoop.current_phase;
|
|
1980
|
-
const
|
|
2030
|
+
const allCriticSlots = advancedLoop.slots.filter((s) => s.role === 'critic');
|
|
2031
|
+
const criticSlots = req.ideation_schedule === 'parallel'
|
|
2032
|
+
? allCriticSlots
|
|
2033
|
+
: allCriticSlots.slice(0, 1);
|
|
1981
2034
|
for (const slot of criticSlots) {
|
|
1982
2035
|
if (!slot.agent)
|
|
1983
2036
|
continue;
|
|
@@ -1997,7 +2050,10 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1997
2050
|
const briefResult = buildIdeationBrief({
|
|
1998
2051
|
thread: advancedLoop,
|
|
1999
2052
|
slotRole: slot.role,
|
|
2053
|
+
slotPerspective: slot.perspective,
|
|
2000
2054
|
memoryProvider: provider,
|
|
2055
|
+
seedText: req.task,
|
|
2056
|
+
scopeHints: req.scope ? [req.scope] : [],
|
|
2001
2057
|
});
|
|
2002
2058
|
// pln#626 Phase 2 (Option B) — spawn the critic as a worktree-isolated
|
|
2003
2059
|
// worker, mirroring the intent=assign / review chain. Each critic gets
|
|
@@ -2009,7 +2065,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
2009
2065
|
// stray edit harmless (it lands in the throwaway checkout, not master).
|
|
2010
2066
|
const criticScope = `ideate-loop:${loopId}:${slot.slot_id}`;
|
|
2011
2067
|
const criticDescription = `Ideation critic turn for loop ${loopId} slot ${slot.slot_id} (phase ${advancedLoop.current_phase}). `
|
|
2012
|
-
+ `Critique
|
|
2068
|
+
+ `Critique proposal artifact ${proposalArtifactId} and reply with evidence — do not edit code.`;
|
|
2013
2069
|
try {
|
|
2014
2070
|
const claimResult = createCoordinatorClaim({
|
|
2015
2071
|
agent: slot.agent,
|
|
@@ -2210,9 +2266,15 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
2210
2266
|
proposal_artifact_id: proposalArtifactId,
|
|
2211
2267
|
selected_targets: explicitTargets ? req.targetAgents : [],
|
|
2212
2268
|
mode: explicitTargets ? 'multi_agent' : 'single_agent',
|
|
2269
|
+
...(explicitTargets ? {
|
|
2270
|
+
ideation_schedule: req.ideation_schedule,
|
|
2271
|
+
pending_critics: Math.max(0, req.targetAgents.length - dispatchedCritics),
|
|
2272
|
+
} : {}),
|
|
2213
2273
|
dispatched_critics: dispatchedCritics,
|
|
2214
2274
|
current_phase: dispatchedPhase,
|
|
2215
|
-
|
|
2275
|
+
task_bytes: Buffer.byteLength(req.task, 'utf8'),
|
|
2276
|
+
task_truncated: false,
|
|
2277
|
+
delivery_plan: preparedCritics.map((p) => compactDeliveryEntry(p.entry)),
|
|
2216
2278
|
...(ideateExecStatus
|
|
2217
2279
|
? { execution_status: ideateExecStatus }
|
|
2218
2280
|
: explicitTargets
|
|
@@ -20,7 +20,8 @@ import { deleteMemoryItem, updateMemoryItem } from '../core/operations/memory-mu
|
|
|
20
20
|
import { assessMemoryPressure, buildCompactionTemplate, applyCompaction } from '../core/gc-semantic.js';
|
|
21
21
|
import { createRuntimeNote } from './runtime-note.js';
|
|
22
22
|
import { createCandidateFromInput } from './reflect.js';
|
|
23
|
-
import { harvestCandidates } from './harvest.js';
|
|
23
|
+
import { harvestCandidates, harvestLaneResults, integrateLaneResults } from './harvest.js';
|
|
24
|
+
import { dispatchReviewLoopTurn } from '../core/review-loop-turn-dispatch.js';
|
|
24
25
|
import { ensureTrust, scanMcpWriteText, appendSecurityWarnings } from './mcp-write-support.js';
|
|
25
26
|
import { toolResponse, createToolErrorResponse, } from './mcp-contract.js';
|
|
26
27
|
function scoreKeywordMatches(text, patterns) {
|
|
@@ -448,4 +449,89 @@ export function handleBclawHarvestCandidates(payload, _ctx) {
|
|
|
448
449
|
}),
|
|
449
450
|
};
|
|
450
451
|
}
|
|
452
|
+
/** MCP parity for the CLI lane-result harvest path (distinct from candidates). */
|
|
453
|
+
export async function handleBclawHarvestLane(payload) {
|
|
454
|
+
const { args, cwd, connectionSessionId } = payload;
|
|
455
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
456
|
+
if (resolved.error) {
|
|
457
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
458
|
+
}
|
|
459
|
+
const assignmentId = typeof args.assignmentId === 'string' ? args.assignmentId : undefined;
|
|
460
|
+
const all = args.all === true;
|
|
461
|
+
if (!assignmentId && !all) {
|
|
462
|
+
return { response: createToolErrorResponse('validation_error', 'Provide assignmentId, or set all=true to scan every managed lane.') };
|
|
463
|
+
}
|
|
464
|
+
if (assignmentId && all) {
|
|
465
|
+
return { response: createToolErrorResponse('validation_error', 'assignmentId and all=true are mutually exclusive.') };
|
|
466
|
+
}
|
|
467
|
+
const worktreePaths = Array.isArray(args.worktreePaths) ? args.worktreePaths.filter((value) => typeof value === 'string') : undefined;
|
|
468
|
+
const dryRun = args.dryRun === true;
|
|
469
|
+
const actor = resolved.identity.agent_name;
|
|
470
|
+
if (args.integrate === true) {
|
|
471
|
+
const integrated = integrateLaneResults({ assignmentId, worktreePaths, dryRun, cwd, agent: actor });
|
|
472
|
+
const dispatchedTurns = [];
|
|
473
|
+
if (!dryRun) {
|
|
474
|
+
for (const next of integrated.next_turns) {
|
|
475
|
+
const dispatched = await dispatchReviewLoopTurn({
|
|
476
|
+
loopId: next.loop_id,
|
|
477
|
+
slot: { slot_id: next.slot_id, role: next.role, agent: next.agent, agent_id: next.agent_id },
|
|
478
|
+
phase: next.phase,
|
|
479
|
+
task: next.task,
|
|
480
|
+
dispatcherAgent: actor,
|
|
481
|
+
dispatcherAgentId: resolved.identity.agent_id,
|
|
482
|
+
cwd,
|
|
483
|
+
});
|
|
484
|
+
dispatchedTurns.push({
|
|
485
|
+
loop_id: next.loop_id,
|
|
486
|
+
agent: next.agent,
|
|
487
|
+
iteration: next.iteration,
|
|
488
|
+
execution_status: dispatched.execution_status,
|
|
489
|
+
error: dispatched.error,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
response: toolResponse({
|
|
495
|
+
content: [{ type: 'text', text: `✔ Lane integrate${dryRun ? ' (dry-run)' : ''}: ${integrated.integrated.length} integrated, ${dispatchedTurns.length} re-dispatched, ${integrated.errors.length} error(s).` }],
|
|
496
|
+
...integrated,
|
|
497
|
+
dispatched_turns: dispatchedTurns,
|
|
498
|
+
dry_run: dryRun,
|
|
499
|
+
}),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const harvested = harvestLaneResults({ assignmentId, worktreePaths, dryRun, cwd, agent: actor });
|
|
503
|
+
const continuationActions = harvested.continuations.flatMap((continuation) => {
|
|
504
|
+
const next = continuation.next_expected;
|
|
505
|
+
if (!next)
|
|
506
|
+
return [];
|
|
507
|
+
if (next.action === 'turn' && next.slot_id) {
|
|
508
|
+
return [{
|
|
509
|
+
tool: 'bclaw_loop',
|
|
510
|
+
args: { intent: 'turn', loop_id: continuation.loop_id, slot_id: next.slot_id, dispatch: true },
|
|
511
|
+
when: next.reason ?? 'dispatch the next sequential loop participant',
|
|
512
|
+
}];
|
|
513
|
+
}
|
|
514
|
+
if (next.action === 'advance') {
|
|
515
|
+
return [{ tool: 'bclaw_loop', args: { intent: 'advance', loop_id: continuation.loop_id }, when: 'the current phase gate is satisfied' }];
|
|
516
|
+
}
|
|
517
|
+
return [{ tool: 'bclaw_loop', args: { intent: 'get', loop_id: continuation.loop_id }, when: next.reason ?? `inspect the expected ${next.action} action` }];
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
response: toolResponse({
|
|
521
|
+
content: [{ type: 'text', text: `✔ Lane harvest${dryRun ? ' (dry-run)' : ''}: ${harvested.harvested.length} harvested, ${harvested.skipped.length} skipped, ${harvested.errors.length} error(s), ${harvested.warnings.length} warning(s).` }],
|
|
522
|
+
structuredContent: {
|
|
523
|
+
harvested: harvested.harvested,
|
|
524
|
+
skipped: harvested.skipped,
|
|
525
|
+
errors: harvested.errors,
|
|
526
|
+
warnings: harvested.warnings,
|
|
527
|
+
continuations: harvested.continuations,
|
|
528
|
+
dry_run: dryRun,
|
|
529
|
+
next_actions: [
|
|
530
|
+
...continuationActions,
|
|
531
|
+
...harvested.warnings.flatMap((warning) => warning.next_actions ?? []),
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
}),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
451
537
|
//# sourceMappingURL=mcp-write-memory.js.map
|
package/dist/commands/mcp.js
CHANGED
|
@@ -48,7 +48,7 @@ import { handleBclawClaim, handleBclawReleaseClaim, handleBclawSessionStart, han
|
|
|
48
48
|
// Sequence write handlers extracted in pln#622 PR4.
|
|
49
49
|
import { handleBclawCreateSequence, handleBclawUpdateSequence, handleBclawDeleteSequence, } from './mcp-write-sequences.js';
|
|
50
50
|
// Memory write handlers extracted in pln#622 PR4.
|
|
51
|
-
import { handleBclawWriteNote, handleBclawQuickCapture, handleBclawCompact, handleBclawDeleteMemory, handleBclawUpdateMemory, handleBclawHarvestCandidates, } from './mcp-write-memory.js';
|
|
51
|
+
import { handleBclawWriteNote, handleBclawQuickCapture, handleBclawCompact, handleBclawDeleteMemory, handleBclawUpdateMemory, handleBclawHarvestCandidates, handleBclawHarvestLane, } from './mcp-write-memory.js';
|
|
52
52
|
// Admin / provisioning write handlers extracted in pln#622 PR4.
|
|
53
53
|
import { handleBclawSetup, handleBclawInitProject, handleBclawAddCapability, handleBclawAddTool, } from './mcp-write-admin.js';
|
|
54
54
|
// Entity write handlers extracted in pln#622 PR4.
|
|
@@ -1097,19 +1097,32 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1097
1097
|
const { JsonlBackend } = await import('../core/code-map/backend.js');
|
|
1098
1098
|
const be = new JsonlBackend();
|
|
1099
1099
|
// Session-scoped project selection is authoritative for Code Map too.
|
|
1100
|
-
|
|
1100
|
+
// An explicit project selector wins, matching the canonical read grammar.
|
|
1101
|
+
let codeCwd = scopeInfo.cwd;
|
|
1102
|
+
let codeScope = scopeInfo;
|
|
1103
|
+
if (typeof args.project === 'string' && args.project.trim()) {
|
|
1104
|
+
codeCwd = resolveProjectCwd(args.project.trim(), codeCwd);
|
|
1105
|
+
codeScope = {
|
|
1106
|
+
cwd: codeCwd,
|
|
1107
|
+
active_source: 'explicit',
|
|
1108
|
+
resolved_project: projectInfoForCwd(codeCwd),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1101
1111
|
if (name === 'bclaw_code_status') {
|
|
1102
1112
|
const status = await be.status({ cwd: codeCwd, cascade: args.cascade === true });
|
|
1103
1113
|
const diskVersion = readDiskBrainclawVersion();
|
|
1104
1114
|
return {
|
|
1105
1115
|
response: toolResponse({
|
|
1106
|
-
content: [{
|
|
1116
|
+
content: [{
|
|
1117
|
+
type: 'text',
|
|
1118
|
+
text: `Code Map: path=${status.store_exists ? 'present' : 'absent'}, index=${status.index_exists ? 'ready' : status.index_manifest_exists ? 'invalid' : 'missing'} — freshness=${status.freshness_badge.freshness}`,
|
|
1119
|
+
}],
|
|
1107
1120
|
structuredContent: {
|
|
1108
1121
|
...status,
|
|
1109
1122
|
freshness_badge: status.freshness_badge,
|
|
1110
1123
|
mcp_resolution: {
|
|
1111
|
-
active_source:
|
|
1112
|
-
resolved_project:
|
|
1124
|
+
active_source: codeScope.active_source,
|
|
1125
|
+
resolved_project: codeScope.resolved_project,
|
|
1113
1126
|
server_version: getInstalledBrainclawVersion(),
|
|
1114
1127
|
disk_version: diskVersion,
|
|
1115
1128
|
restart_required: diskVersion !== '0.0.0' && diskVersion !== getInstalledBrainclawVersion(),
|
|
@@ -1136,12 +1149,27 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1136
1149
|
};
|
|
1137
1150
|
}
|
|
1138
1151
|
}
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
1152
|
+
const { startCodeRefreshJob, summarizeCodeRefreshJob } = await import('../core/code-map/refresh-jobs.js');
|
|
1153
|
+
const job = startCodeRefreshJob(codeCwd, scope);
|
|
1154
|
+
const accepted = job.status !== 'failed' && job.scope === scope;
|
|
1155
|
+
const acknowledgement = accepted
|
|
1156
|
+
? `Code Map refresh accepted: job=${job.job_id}, scope=${job.scope}, project=${codeCwd}.`
|
|
1157
|
+
: job.status === 'failed'
|
|
1158
|
+
? `Code Map refresh failed to start: job=${job.job_id}, project=${codeCwd}, error=${job.error ?? 'unknown'}.`
|
|
1159
|
+
: `Code Map refresh not queued: active job=${job.job_id} has scope=${job.scope}; requested scope=${scope}.`;
|
|
1141
1160
|
return {
|
|
1142
1161
|
response: toolResponse({
|
|
1143
|
-
content: [{ type: 'text', text:
|
|
1144
|
-
structuredContent: {
|
|
1162
|
+
content: [{ type: 'text', text: `${acknowledgement} Follow with bclaw_code_status${typeof args.project === 'string' ? `(project=${JSON.stringify(args.project)})` : ''}.` }],
|
|
1163
|
+
structuredContent: {
|
|
1164
|
+
accepted,
|
|
1165
|
+
requested_scope: scope,
|
|
1166
|
+
...summarizeCodeRefreshJob(job),
|
|
1167
|
+
next_actions: [{
|
|
1168
|
+
tool: 'bclaw_code_status',
|
|
1169
|
+
args: typeof args.project === 'string' ? { project: args.project } : {},
|
|
1170
|
+
when: 'follow refresh progress and terminal outcome',
|
|
1171
|
+
}],
|
|
1172
|
+
},
|
|
1145
1173
|
}),
|
|
1146
1174
|
};
|
|
1147
1175
|
}
|
|
@@ -1226,8 +1254,15 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1226
1254
|
};
|
|
1227
1255
|
}
|
|
1228
1256
|
if (MCP_READ_TOOLS.some((tool) => tool.name === name) || LEGACY_READ_TOOL_HANDLERS.has(name)) {
|
|
1257
|
+
const response = appendLegacyMcpToolWarning(toolResponse(handleMcpReadToolCall(name, args, { cwd, connectionSessionId, effectiveScope: scopeInfo })), name);
|
|
1258
|
+
const switchedSessionId = name === 'bclaw_switch'
|
|
1259
|
+
? response.structuredContent?.session_id
|
|
1260
|
+
: undefined;
|
|
1229
1261
|
return {
|
|
1230
|
-
response
|
|
1262
|
+
response,
|
|
1263
|
+
...(typeof switchedSessionId === 'string' && switchedSessionId
|
|
1264
|
+
? { nextConnectionSessionId: switchedSessionId }
|
|
1265
|
+
: {}),
|
|
1231
1266
|
};
|
|
1232
1267
|
}
|
|
1233
1268
|
// Resolve model once for all write operations
|
|
@@ -1736,6 +1771,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1736
1771
|
if (name === 'bclaw_harvest_candidates') {
|
|
1737
1772
|
return handleBclawHarvestCandidates(payload, writeMemoryCtx);
|
|
1738
1773
|
}
|
|
1774
|
+
if (name === 'bclaw_harvest') {
|
|
1775
|
+
return await handleBclawHarvestLane(payload);
|
|
1776
|
+
}
|
|
1739
1777
|
// ── Canonical CRUD verbs (Phase 3 slice 3b) ──────────────────────
|
|
1740
1778
|
//
|
|
1741
1779
|
// Thin wrappers around src/core/entity-operations.ts. Behind
|
|
@@ -1835,6 +1873,17 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1835
1873
|
};
|
|
1836
1874
|
}
|
|
1837
1875
|
const result = listEntities(entity, targetCwd, filter);
|
|
1876
|
+
const requestedFields = Array.isArray(args.fields)
|
|
1877
|
+
? args.fields.filter((field) => typeof field === 'string' && field.length > 0)
|
|
1878
|
+
: [];
|
|
1879
|
+
if (requestedFields.length > 0) {
|
|
1880
|
+
result.items = result.items.map((item) => {
|
|
1881
|
+
if (!item || typeof item !== 'object')
|
|
1882
|
+
return item;
|
|
1883
|
+
const row = item;
|
|
1884
|
+
return Object.fromEntries(requestedFields.filter((field) => row[field] !== undefined).map((field) => [field, row[field]]));
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1838
1887
|
// pln#491 — bound the payload (count is already capped by applyPaging;
|
|
1839
1888
|
// this caps SIZE) so a verbose result set never overflows the MCP token
|
|
1840
1889
|
// cap and silently pushes the agent to the CLI (trp#449). Advertises
|
|
@@ -1850,7 +1899,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1850
1899
|
{ tool: 'bclaw_get', args: { entity, id: '<id from items>', ...(args.project ? { project: args.project } : {}), ...(args.budget_tokens ? { budget_tokens: args.budget_tokens } : {}) }, when: 'to read one item in full' },
|
|
1851
1900
|
];
|
|
1852
1901
|
if (bounded.has_more) {
|
|
1853
|
-
nextActions.push({ tool: 'bclaw_find', args: { entity, filter: { ...filter, offset: bounded.next_offset }, ...(args.project ? { project: args.project } : {}), ...(args.budget_tokens ? { budget_tokens: args.budget_tokens } : {}) }, when: 'to fetch the next page' });
|
|
1902
|
+
nextActions.push({ tool: 'bclaw_find', args: { entity, filter: { ...filter, offset: bounded.next_offset }, ...(requestedFields.length ? { fields: requestedFields } : {}), ...(args.project ? { project: args.project } : {}), ...(args.budget_tokens ? { budget_tokens: args.budget_tokens } : {}) }, when: 'to fetch the next page' });
|
|
1854
1903
|
}
|
|
1855
1904
|
// structuredContent is the canonical MCP return channel that clients
|
|
1856
1905
|
// (VS Code extension, Codex, etc.) read for machine-parseable data.
|
package/dist/commands/switch.js
CHANGED
|
@@ -72,7 +72,14 @@ export function switchProject(projectRef, options = {}) {
|
|
|
72
72
|
...session,
|
|
73
73
|
active_project: { path: resolved, name: projectName, switched_at: now },
|
|
74
74
|
}, cwd);
|
|
75
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
switched: true,
|
|
77
|
+
path: resolved,
|
|
78
|
+
name: projectName,
|
|
79
|
+
scope: 'session',
|
|
80
|
+
workspace_root: wsRoot,
|
|
81
|
+
session_id: session.session_id,
|
|
82
|
+
};
|
|
76
83
|
}
|
|
77
84
|
if (sessionOnly) {
|
|
78
85
|
throw new Error('Cannot switch project without an active agent session. Start with bclaw_work or bclaw_session_start first.');
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* `freshness_badge`, locking the response shape for later sprints.
|
|
10
10
|
*/
|
|
11
11
|
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import fs from 'node:fs';
|
|
12
13
|
import path from 'node:path';
|
|
13
|
-
import { readManifest, readShard
|
|
14
|
+
import { readManifest, readShard } from './store.js';
|
|
14
15
|
import { refresh as runRefresh } from './refresh.js';
|
|
15
16
|
import { applyGitHeadDrift, withFreshness } from './freshness.js';
|
|
16
17
|
import { brief as runBrief, find as runFind } from './query.js';
|
|
@@ -21,8 +22,9 @@ import { resolveTraversal, aggregateFind, aggregateBrief } from './aggregate.js'
|
|
|
21
22
|
import { defaultMemoryReader } from './memory-reader.js';
|
|
22
23
|
import { inspectNestedProjects, refreshWorkspaceCascade } from './cascade.js';
|
|
23
24
|
import { latestCascadeRefreshJob, summarizeCascadeRefreshJob } from './cascade-jobs.js';
|
|
25
|
+
import { latestCodeRefreshJob, summarizeCodeRefreshJob } from './refresh-jobs.js';
|
|
24
26
|
import { loadConfig } from '../config.js';
|
|
25
|
-
import { codeMapDir } from './paths.js';
|
|
27
|
+
import { codeMapDir, manifestPath } from './paths.js';
|
|
26
28
|
/** spec §9 caps the brief reading list at 12 files. */
|
|
27
29
|
export const BRIEF_FILE_CAP = 12;
|
|
28
30
|
/**
|
|
@@ -116,9 +118,12 @@ function buildCascadeStatus(rootCwd) {
|
|
|
116
118
|
const discovery = inspectNestedProjects(root);
|
|
117
119
|
const children = discovery.projects.map((abs) => {
|
|
118
120
|
const m = readManifest(abs);
|
|
121
|
+
const storePath = codeMapDir(abs);
|
|
119
122
|
return {
|
|
120
123
|
path: path.relative(root, abs).replace(/\\/g, '/') || '.',
|
|
121
|
-
store_exists:
|
|
124
|
+
store_exists: fs.existsSync(storePath),
|
|
125
|
+
index_exists: m !== null,
|
|
126
|
+
index_manifest_exists: fs.existsSync(manifestPath(abs)),
|
|
122
127
|
freshness: m ? m.freshness.status : 'missing_index',
|
|
123
128
|
files_indexed: m ? m.stats.files_indexed : null,
|
|
124
129
|
...(m && m.stats.files_indexed === 0 ? { reason: 'no_eligible_files' } : {}),
|
|
@@ -157,9 +162,13 @@ export class JsonlBackend {
|
|
|
157
162
|
store_path: codeMapDir(projectRoot, input.preferredDirName),
|
|
158
163
|
};
|
|
159
164
|
const manifest = readManifest(input.cwd, input.preferredDirName);
|
|
165
|
+
const storePathExists = fs.existsSync(resolution.store_path);
|
|
166
|
+
const manifestFileExists = fs.existsSync(manifestPath(input.cwd, input.preferredDirName));
|
|
160
167
|
const result = manifest
|
|
161
168
|
? {
|
|
162
|
-
store_exists:
|
|
169
|
+
store_exists: storePathExists,
|
|
170
|
+
index_exists: true,
|
|
171
|
+
index_manifest_exists: true,
|
|
163
172
|
resolution,
|
|
164
173
|
freshness_badge: this.withHeadDrift(badge(manifest.freshness.status, {
|
|
165
174
|
stale_file_count: manifest.freshness.stale_file_count,
|
|
@@ -172,7 +181,9 @@ export class JsonlBackend {
|
|
|
172
181
|
},
|
|
173
182
|
}
|
|
174
183
|
: {
|
|
175
|
-
store_exists:
|
|
184
|
+
store_exists: storePathExists,
|
|
185
|
+
index_exists: false,
|
|
186
|
+
index_manifest_exists: manifestFileExists,
|
|
176
187
|
resolution,
|
|
177
188
|
freshness_badge: badge('missing_index'),
|
|
178
189
|
stats: null,
|
|
@@ -183,6 +194,9 @@ export class JsonlBackend {
|
|
|
183
194
|
if (input.cascade && isMultiProjectWorkspace(input.cwd)) {
|
|
184
195
|
result.cascade = buildCascadeStatus(input.cwd);
|
|
185
196
|
}
|
|
197
|
+
const latestRefresh = latestCodeRefreshJob(projectRoot);
|
|
198
|
+
if (latestRefresh)
|
|
199
|
+
result.refresh_job = summarizeCodeRefreshJob(latestRefresh);
|
|
186
200
|
return result;
|
|
187
201
|
}
|
|
188
202
|
/**
|