brainclaw 1.28.2 → 1.28.4
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/commands/harvest.js +67 -25
- package/dist/commands/loops-handlers.js +28 -1
- package/dist/commands/mcp-catalog.js +25 -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 +97 -26
- package/dist/commands/mcp-write-entities.js +5 -2
- package/dist/commands/mcp-write-memory.js +87 -1
- package/dist/commands/mcp.js +41 -9
- 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/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 +80 -8
- package/dist/core/entity-registry.js +3 -3
- package/dist/core/execution-adapters.js +18 -1
- 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/spawn-check.js +9 -1
- package/dist/core/worktree.js +14 -7
- package/dist/facts.js +10 -9
- package/dist/facts.json +9 -8
- package/docs/cli.md +35 -2
- package/docs/code-map.md +20 -9
- package/docs/concepts/ideation-loop.md +35 -14
- package/docs/integrations/mcp.md +15 -5
- package/docs/mcp-schema-changelog.md +72 -6
- package/package.json +1 -1
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
import crypto from 'node:crypto';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
15
17
|
import { spawnSync } from 'node:child_process';
|
|
16
18
|
import { buildClaimEnvPrefix } from '../core/execution-profile.js';
|
|
17
19
|
import { resolveProjectCwd } from '../core/cross-project.js';
|
|
@@ -21,8 +23,7 @@ import { appendAuditEntry } from '../core/audit.js';
|
|
|
21
23
|
import { nowISO } from '../core/ids.js';
|
|
22
24
|
import { validateMcpField } from '../core/input-validation.js';
|
|
23
25
|
import { generateCandidateIdWithLabel, saveCandidate } from '../core/candidates.js';
|
|
24
|
-
import { DEFAULT_PROTOCOLS } from '../core/loops/types.js';
|
|
25
|
-
import { capLoopArtifactBody } from '../core/loops/result-reducers.js';
|
|
26
|
+
import { DEFAULT_PROTOCOLS, LOOP_PROPOSAL_BODY_MAX_BYTES } from '../core/loops/types.js';
|
|
26
27
|
import { validateLoopProjectResolution } from '../core/loops/project-resolution.js';
|
|
27
28
|
import { coordinateNextActions, dispatchNextActions } from '../core/next-actions.js';
|
|
28
29
|
import { agentValidationFailedWarning, consultAutoExecuteNoOpWarning, planAlreadyAssignedWarning, pushStructuredWarning, scopeAlreadyClaimedWarning, } from '../core/warnings.js';
|
|
@@ -308,6 +309,25 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
308
309
|
return { response: createToolErrorResponse('validation_error', parseResult.error.message) };
|
|
309
310
|
}
|
|
310
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
|
+
}
|
|
311
331
|
// pln#511 step 2 — preset selector validation. Presets are kind-
|
|
312
332
|
// specific in v1: only intent='ideate' carries them. Unknown names
|
|
313
333
|
// are rejected up-front against the registry so the handler never
|
|
@@ -387,9 +407,17 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
387
407
|
// for state-mutating helpers; the outer `cwd` (source) stays in scope
|
|
388
408
|
// for the few cases that genuinely need source attribution.
|
|
389
409
|
const dispatchCwd = resolveProjectCwd(req.project, cwd);
|
|
390
|
-
const isCrossProject = dispatchCwd !== cwd;
|
|
410
|
+
const isCrossProject = path.resolve(dispatchCwd) !== path.resolve(cwd);
|
|
391
411
|
if (isCrossProject && req.autoExecute !== false) {
|
|
392
|
-
|
|
412
|
+
return {
|
|
413
|
+
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.`, {
|
|
414
|
+
next_actions: [{
|
|
415
|
+
tool: 'bclaw_coordinate',
|
|
416
|
+
args: { ...req, autoExecute: false },
|
|
417
|
+
when: 'create an inbox-only cross-project assignment that the target agent will pick up with bclaw_work',
|
|
418
|
+
}],
|
|
419
|
+
}),
|
|
420
|
+
};
|
|
393
421
|
}
|
|
394
422
|
const effectiveAutoExecute = isCrossProject ? false : req.autoExecute;
|
|
395
423
|
// pln#692 P0 — an explicit checkout ref is part of admission, not worktree
|
|
@@ -418,8 +446,9 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
418
446
|
// pln#692 P0 — admission must prove that a multi-agent ideation request can
|
|
419
447
|
// satisfy the first worker-produced phase gate BEFORE openLoop (or any
|
|
420
448
|
// identity/claim/assignment mutation). The default critique phase requires
|
|
421
|
-
// three distinct critique artifacts
|
|
422
|
-
//
|
|
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.
|
|
423
452
|
if (req.intent === 'ideate'
|
|
424
453
|
&& req.preset !== 'bootstrap'
|
|
425
454
|
&& Array.isArray(req.targetAgents)
|
|
@@ -427,23 +456,27 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
427
456
|
const critiquePhase = DEFAULT_PROTOCOLS.ideation.phases.find((phase) => phase.name === 'critique');
|
|
428
457
|
const gate = critiquePhase?.advance_gate;
|
|
429
458
|
const requiredCritics = gate?.kind === 'min_artifacts_by_type' ? gate.n : 0;
|
|
430
|
-
const
|
|
431
|
-
const checks = uniqueTargets.map((agent) => ({
|
|
459
|
+
const checks = req.targetAgents.map((agent, instanceIndex) => ({
|
|
432
460
|
agent,
|
|
461
|
+
instanceIndex,
|
|
433
462
|
check: validateAgentForDispatch(agent, { requireSpawnable: true }),
|
|
434
463
|
}));
|
|
435
464
|
const executableTargets = checks.filter(({ check }) => check.valid).map(({ agent }) => agent);
|
|
436
|
-
const invalidTargets = checks.filter(({ check }) => !check.valid).map(({ agent, check }) => ({
|
|
465
|
+
const invalidTargets = checks.filter(({ check }) => !check.valid).map(({ agent, instanceIndex, check }) => ({
|
|
437
466
|
agent,
|
|
467
|
+
instance_index: instanceIndex,
|
|
438
468
|
code: check.code,
|
|
439
469
|
reason: check.reason,
|
|
440
470
|
}));
|
|
441
471
|
if (requiredCritics > 0 && executableTargets.length < requiredCritics) {
|
|
442
472
|
const availableTargets = getSpawnableAgents()
|
|
443
473
|
.map((profile) => profile.name)
|
|
444
|
-
.filter((agent, index, all) =>
|
|
474
|
+
.filter((agent, index, all) => all.indexOf(agent) === index)
|
|
445
475
|
.filter((agent) => validateAgentForDispatch(agent, { requireSpawnable: true }).valid);
|
|
446
|
-
const
|
|
476
|
+
const recoveryAgent = executableTargets[0] ?? availableTargets[0];
|
|
477
|
+
const recoveryTargets = recoveryAgent
|
|
478
|
+
? Array.from({ length: requiredCritics }, () => recoveryAgent)
|
|
479
|
+
: [];
|
|
447
480
|
const nextActions = recoveryTargets.length >= requiredCritics
|
|
448
481
|
? [{
|
|
449
482
|
tool: 'bclaw_coordinate',
|
|
@@ -451,21 +484,20 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
451
484
|
intent: 'ideate', task: req.task, scope: req.scope,
|
|
452
485
|
targetAgents: recoveryTargets, autoExecute: effectiveAutoExecute !== false,
|
|
453
486
|
},
|
|
454
|
-
when: `retry with at least ${requiredCritics}
|
|
487
|
+
when: `retry with at least ${requiredCritics} executable critic instances`,
|
|
455
488
|
}]
|
|
456
489
|
: [{
|
|
457
490
|
tool: 'bclaw_context',
|
|
458
491
|
args: { kind: 'execution', includeAgentTooling: true },
|
|
459
|
-
when:
|
|
492
|
+
when: 'configure at least one spawnable critic identity before retrying',
|
|
460
493
|
}];
|
|
461
494
|
return {
|
|
462
|
-
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}`, {
|
|
463
496
|
gate: { phase: 'critique', kind: gate?.kind, expected: requiredCritics, observed: executableTargets.length },
|
|
464
497
|
requested_targets: req.targetAgents,
|
|
465
498
|
executable_targets: executableTargets,
|
|
466
499
|
invalid_targets: invalidTargets,
|
|
467
500
|
blockers: [
|
|
468
|
-
...(uniqueTargets.length < req.targetAgents.length ? ['duplicate target identities do not add executable capacity'] : []),
|
|
469
501
|
...(invalidTargets.length > 0 ? ['one or more requested targets are not spawnable'] : []),
|
|
470
502
|
`missing executable critic capacity: ${requiredCritics - executableTargets.length}`,
|
|
471
503
|
],
|
|
@@ -729,6 +761,18 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
729
761
|
contextEnvelope: options?.contextEnvelope,
|
|
730
762
|
});
|
|
731
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
|
+
};
|
|
732
776
|
const toMessageSummary = (deliveryPlan) => deliveryPlan.map((entry) => ({
|
|
733
777
|
agent: entry.agent,
|
|
734
778
|
message_id: entry.message_id,
|
|
@@ -1008,7 +1052,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1008
1052
|
// existing length===0 guard skips loop creation. Skipped when open_loop
|
|
1009
1053
|
// is off, preflight=false, or BRAINCLAW_NO_SPAWN is set (handled inside
|
|
1010
1054
|
// preflightAgents). Cross-project dispatch never auto-spawns, so skip.
|
|
1011
|
-
if (req.open_loop === true && req.preflight !== false && !
|
|
1055
|
+
if (req.open_loop === true && req.preflight !== false && !isCrossProject && loopReviewerAgents.length > 0) {
|
|
1012
1056
|
try {
|
|
1013
1057
|
const { preflightAgents } = await import('../core/spawn-check.js');
|
|
1014
1058
|
const pf = await preflightAgents(loopReviewerAgents, { cwd: dispatchCwd });
|
|
@@ -1158,7 +1202,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1158
1202
|
// (that is the double-spawn hole), and do NOT release the (possibly shared) claim; leave
|
|
1159
1203
|
// the slot for reconcile/self-heal. LEGACY: the unchanged inline mint runs.
|
|
1160
1204
|
let usedTurnOwned = false;
|
|
1161
|
-
if (turnOwnedReviewEnabled() && !
|
|
1205
|
+
if (turnOwnedReviewEnabled() && !isCrossProject) {
|
|
1162
1206
|
const prep = prepareTurnOwnedReviewDispatch({
|
|
1163
1207
|
loopId: loop.id,
|
|
1164
1208
|
slotId: slot.slot_id,
|
|
@@ -1837,11 +1881,19 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1837
1881
|
},
|
|
1838
1882
|
];
|
|
1839
1883
|
if (explicitTargets) {
|
|
1840
|
-
|
|
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()) {
|
|
1841
1890
|
const criticIdentity = findAgentIdentityByName(agent, dispatchCwd) ?? ensureAgentRegisteredForDispatch(agent, dispatchCwd);
|
|
1842
1891
|
slots.push({
|
|
1843
1892
|
role: 'critic',
|
|
1844
1893
|
agent,
|
|
1894
|
+
perspective: req.criticPerspectives?.[index]
|
|
1895
|
+
?? defaultPerspectives[index]
|
|
1896
|
+
?? `Challenge the conversation from an independent perspective ${index + 1}; avoid repeating prior contributions.`,
|
|
1845
1897
|
...(criticIdentity?.agent_id ? { agent_id: criticIdentity.agent_id } : {}),
|
|
1846
1898
|
});
|
|
1847
1899
|
}
|
|
@@ -1869,7 +1921,12 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1869
1921
|
stop_condition: presetSelected.stop_condition,
|
|
1870
1922
|
protocol: presetSelected.protocol,
|
|
1871
1923
|
}
|
|
1872
|
-
: {
|
|
1924
|
+
: {
|
|
1925
|
+
protocol: {
|
|
1926
|
+
iteration: DEFAULT_PROTOCOLS.ideation.iteration,
|
|
1927
|
+
ideation_schedule: req.ideation_schedule,
|
|
1928
|
+
},
|
|
1929
|
+
}),
|
|
1873
1930
|
}, dispatchCwd);
|
|
1874
1931
|
loopId = loop.id;
|
|
1875
1932
|
artifacts.push({ type: 'loop', id: loop.id });
|
|
@@ -1883,14 +1940,13 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1883
1940
|
// loop doesn't contain. The task text is already captured on
|
|
1884
1941
|
// the thread (title + goal).
|
|
1885
1942
|
if (!presetSelected) {
|
|
1886
|
-
const proposalBody = capLoopArtifactBody(req.task);
|
|
1887
1943
|
const updated = add_artifact({
|
|
1888
1944
|
id: loop.id,
|
|
1889
1945
|
actor: creatorActor,
|
|
1890
1946
|
artifact: {
|
|
1891
1947
|
phase: 'proposal',
|
|
1892
1948
|
type: 'proposal',
|
|
1893
|
-
body:
|
|
1949
|
+
body: req.task,
|
|
1894
1950
|
produced_by: creatorActor,
|
|
1895
1951
|
},
|
|
1896
1952
|
}, dispatchCwd);
|
|
@@ -1909,8 +1965,11 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1909
1965
|
};
|
|
1910
1966
|
}
|
|
1911
1967
|
} // end else (non-bootstrap open path)
|
|
1912
|
-
//
|
|
1913
|
-
//
|
|
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.
|
|
1914
1973
|
//
|
|
1915
1974
|
// pln#511 step 2 — initial phase comes from the actual loop's
|
|
1916
1975
|
// first phase, not a hardcoded 'proposal'. Presets like bootstrap
|
|
@@ -1968,7 +2027,10 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1968
2027
|
throw new Error('ideate dispatch: loop disappeared after advance');
|
|
1969
2028
|
}
|
|
1970
2029
|
dispatchedPhase = advancedLoop.current_phase;
|
|
1971
|
-
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);
|
|
1972
2034
|
for (const slot of criticSlots) {
|
|
1973
2035
|
if (!slot.agent)
|
|
1974
2036
|
continue;
|
|
@@ -1988,7 +2050,10 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1988
2050
|
const briefResult = buildIdeationBrief({
|
|
1989
2051
|
thread: advancedLoop,
|
|
1990
2052
|
slotRole: slot.role,
|
|
2053
|
+
slotPerspective: slot.perspective,
|
|
1991
2054
|
memoryProvider: provider,
|
|
2055
|
+
seedText: req.task,
|
|
2056
|
+
scopeHints: req.scope ? [req.scope] : [],
|
|
1992
2057
|
});
|
|
1993
2058
|
// pln#626 Phase 2 (Option B) — spawn the critic as a worktree-isolated
|
|
1994
2059
|
// worker, mirroring the intent=assign / review chain. Each critic gets
|
|
@@ -2000,7 +2065,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
2000
2065
|
// stray edit harmless (it lands in the throwaway checkout, not master).
|
|
2001
2066
|
const criticScope = `ideate-loop:${loopId}:${slot.slot_id}`;
|
|
2002
2067
|
const criticDescription = `Ideation critic turn for loop ${loopId} slot ${slot.slot_id} (phase ${advancedLoop.current_phase}). `
|
|
2003
|
-
+ `Critique
|
|
2068
|
+
+ `Critique proposal artifact ${proposalArtifactId} and reply with evidence — do not edit code.`;
|
|
2004
2069
|
try {
|
|
2005
2070
|
const claimResult = createCoordinatorClaim({
|
|
2006
2071
|
agent: slot.agent,
|
|
@@ -2201,9 +2266,15 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
2201
2266
|
proposal_artifact_id: proposalArtifactId,
|
|
2202
2267
|
selected_targets: explicitTargets ? req.targetAgents : [],
|
|
2203
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
|
+
} : {}),
|
|
2204
2273
|
dispatched_critics: dispatchedCritics,
|
|
2205
2274
|
current_phase: dispatchedPhase,
|
|
2206
|
-
|
|
2275
|
+
task_bytes: Buffer.byteLength(req.task, 'utf8'),
|
|
2276
|
+
task_truncated: false,
|
|
2277
|
+
delivery_plan: preparedCritics.map((p) => compactDeliveryEntry(p.entry)),
|
|
2207
2278
|
...(ideateExecStatus
|
|
2208
2279
|
? { execution_status: ideateExecStatus }
|
|
2209
2280
|
: explicitTargets
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -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}` }],
|
|
@@ -1718,6 +1736,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1718
1736
|
if (name === 'bclaw_harvest_candidates') {
|
|
1719
1737
|
return handleBclawHarvestCandidates(payload, writeMemoryCtx);
|
|
1720
1738
|
}
|
|
1739
|
+
if (name === 'bclaw_harvest') {
|
|
1740
|
+
return await handleBclawHarvestLane(payload);
|
|
1741
|
+
}
|
|
1721
1742
|
// ── Canonical CRUD verbs (Phase 3 slice 3b) ──────────────────────
|
|
1722
1743
|
//
|
|
1723
1744
|
// Thin wrappers around src/core/entity-operations.ts. Behind
|
|
@@ -1817,6 +1838,17 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1817
1838
|
};
|
|
1818
1839
|
}
|
|
1819
1840
|
const result = listEntities(entity, targetCwd, filter);
|
|
1841
|
+
const requestedFields = Array.isArray(args.fields)
|
|
1842
|
+
? args.fields.filter((field) => typeof field === 'string' && field.length > 0)
|
|
1843
|
+
: [];
|
|
1844
|
+
if (requestedFields.length > 0) {
|
|
1845
|
+
result.items = result.items.map((item) => {
|
|
1846
|
+
if (!item || typeof item !== 'object')
|
|
1847
|
+
return item;
|
|
1848
|
+
const row = item;
|
|
1849
|
+
return Object.fromEntries(requestedFields.filter((field) => row[field] !== undefined).map((field) => [field, row[field]]));
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1820
1852
|
// pln#491 — bound the payload (count is already capped by applyPaging;
|
|
1821
1853
|
// this caps SIZE) so a verbose result set never overflows the MCP token
|
|
1822
1854
|
// cap and silently pushes the agent to the CLI (trp#449). Advertises
|
|
@@ -1832,7 +1864,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1832
1864
|
{ 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' },
|
|
1833
1865
|
];
|
|
1834
1866
|
if (bounded.has_more) {
|
|
1835
|
-
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' });
|
|
1867
|
+
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' });
|
|
1836
1868
|
}
|
|
1837
1869
|
// structuredContent is the canonical MCP return channel that clients
|
|
1838
1870
|
// (VS Code extension, Codex, etc.) read for machine-parseable data.
|
|
@@ -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);
|