brainclaw 1.7.5 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -11
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +139 -13
- package/dist/commands/add-step.js +1 -1
- package/dist/commands/bootstrap.js +2 -26
- package/dist/commands/check-security-mcp.js +50 -33
- package/dist/commands/check-security.js +86 -43
- package/dist/commands/claim.js +22 -21
- package/dist/commands/confirm.js +26 -0
- package/dist/commands/context-diff.js +1 -1
- package/dist/commands/dispatch-watch.js +142 -0
- package/dist/commands/doctor.js +113 -2
- package/dist/commands/estimation-report.js +115 -16
- package/dist/commands/harvest.js +502 -16
- package/dist/commands/init.js +123 -21
- package/dist/commands/loops-handlers.js +4 -0
- package/dist/commands/mcp-read-handlers.js +198 -29
- package/dist/commands/mcp.js +615 -92
- package/dist/commands/memory.js +21 -17
- package/dist/commands/migrate.js +81 -17
- package/dist/commands/prune.js +78 -4
- package/dist/commands/reflect.js +26 -20
- package/dist/commands/register-agent.js +57 -1
- package/dist/commands/repair.js +20 -0
- package/dist/commands/session-end.js +15 -6
- package/dist/commands/session-start.js +18 -1
- package/dist/commands/setup-security.js +39 -18
- package/dist/commands/setup.js +26 -27
- package/dist/commands/stale.js +16 -2
- package/dist/commands/uninstall.js +126 -34
- package/dist/commands/update-step.js +6 -0
- package/dist/commands/worktree.js +60 -0
- package/dist/core/actions.js +12 -3
- package/dist/core/agent-capability.js +11 -13
- package/dist/core/agent-files.js +844 -547
- package/dist/core/agent-integrations.js +0 -3
- package/dist/core/agent-inventory.js +67 -0
- package/dist/core/agent-registry.js +163 -29
- package/dist/core/agentrun-reconciler.js +33 -2
- package/dist/core/agentruns.js +7 -1
- package/dist/core/ai-agent-detection.js +31 -44
- package/dist/core/archival.js +15 -9
- package/dist/core/assignment-reconciler.js +56 -0
- package/dist/core/assignment-sweeper.js +127 -4
- package/dist/core/assignments.js +69 -11
- package/dist/core/bootstrap.js +233 -67
- package/dist/core/brainclaw-version.js +22 -0
- package/dist/core/candidates.js +21 -1
- package/dist/core/claims.js +313 -150
- package/dist/core/config.js +6 -1
- package/dist/core/context-diff.js +148 -20
- package/dist/core/context.js +129 -8
- package/dist/core/coordination.js +22 -3
- package/dist/core/dispatch-status.js +109 -5
- package/dist/core/dispatcher.js +65 -11
- package/dist/core/entity-operations.js +45 -24
- package/dist/core/entity-registry.js +31 -5
- package/dist/core/event-log.js +138 -21
- package/dist/core/events/checkpoint.js +258 -0
- package/dist/core/events/genesis.js +220 -0
- package/dist/core/events/journal.js +507 -0
- package/dist/core/events/materialize.js +126 -0
- package/dist/core/events/registry-post-image.js +110 -0
- package/dist/core/events/verify.js +109 -0
- package/dist/core/execution-adapters.js +23 -0
- package/dist/core/execution.js +25 -0
- package/dist/core/facade-schema.js +48 -0
- package/dist/core/gc-semantic.js +130 -5
- package/dist/core/handoff-snapshot.js +68 -0
- package/dist/core/ids.js +19 -8
- package/dist/core/instruction-templates.js +34 -115
- package/dist/core/io.js +39 -3
- package/dist/core/json-store.js +10 -1
- package/dist/core/lock.js +153 -28
- package/dist/core/loops/bootstrap-acquire.js +25 -1
- package/dist/core/loops/facade-schema.js +2 -0
- package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/presets/bootstrap.js +7 -0
- package/dist/core/loops/store.js +17 -0
- package/dist/core/loops/verbs.js +24 -1
- package/dist/core/markdown.js +8 -76
- package/dist/core/mcp-command-resolution.js +245 -0
- package/dist/core/memory-compactor.js +5 -3
- package/dist/core/memory-lifecycle.js +282 -0
- package/dist/core/merge-risk.js +150 -0
- package/dist/core/messaging.js +8 -1
- package/dist/core/migration.js +11 -1
- package/dist/core/observer-mode.js +26 -0
- package/dist/core/operations/memory-mutation.js +90 -65
- package/dist/core/operations/plan.js +27 -1
- package/dist/core/protocol-skills.js +210 -0
- package/dist/core/reflection-safety.js +6 -7
- package/dist/core/reputation.js +84 -2
- package/dist/core/runtime-signals.js +71 -9
- package/dist/core/runtime.js +84 -1
- package/dist/core/schema.js +125 -0
- package/dist/core/security-detectors.js +125 -0
- package/dist/core/security-extract.js +189 -0
- package/dist/core/security-guard.js +107 -29
- package/dist/core/security-packages.js +121 -0
- package/dist/core/security-scoring.js +76 -9
- package/dist/core/security.js +34 -2
- package/dist/core/sequence.js +11 -2
- package/dist/core/setup-flow.js +141 -13
- package/dist/core/spawn-check.js +110 -4
- package/dist/core/staleness.js +109 -1
- package/dist/core/state.js +250 -54
- package/dist/core/store-resolution.js +19 -5
- package/dist/core/worktree.js +169 -7
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/PROTOCOL.md +223 -0
- package/docs/cli.md +11 -10
- package/docs/concepts/coordinator-runbook.md +129 -0
- package/docs/concepts/dispatch-lifecycle.md +17 -0
- package/docs/concepts/event-log-store-critique-A.md +333 -0
- package/docs/concepts/event-log-store-critique-B.md +353 -0
- package/docs/concepts/event-log-store-phase0-measurements.md +58 -0
- package/docs/concepts/event-log-store-proposal-A.md +365 -0
- package/docs/concepts/event-log-store-proposal-B.md +404 -0
- package/docs/concepts/event-log-store.md +928 -0
- package/docs/concepts/identity-model-proposal.md +371 -0
- package/docs/concepts/memory.md +5 -4
- package/docs/concepts/observer-protocol.md +361 -0
- package/docs/concepts/parallel-merge-protocol.md +71 -0
- package/docs/concepts/plans-and-claims.md +43 -0
- package/docs/concepts/skills.md +78 -0
- package/docs/concepts/workspace-bootstrapping.md +61 -0
- package/docs/integrations/agents.md +4 -4
- package/docs/integrations/cline.md +10 -11
- package/docs/integrations/codex.md +2 -2
- package/docs/integrations/continue.md +5 -5
- package/docs/integrations/copilot.md +14 -12
- package/docs/integrations/openclaw.md +7 -6
- package/docs/integrations/overview.md +7 -7
- package/docs/integrations/roo.md +3 -3
- package/docs/integrations/windsurf.md +6 -6
- package/docs/mcp-schema-changelog.md +51 -20
- package/docs/quickstart.md +48 -47
- package/docs/security.md +174 -15
- package/docs/storage.md +4 -2
- package/package.json +8 -6
package/dist/commands/init.js
CHANGED
|
@@ -16,8 +16,11 @@ import { BRAINCLAW_EXCLUSIVE_DIRECTORIES, describeAutoConfigWrite, ensureAgentFi
|
|
|
16
16
|
import { detectAiAgent, detectWslEnvironment } from '../core/ai-agent-detection.js';
|
|
17
17
|
import { buildAiSurfaceInventory, renderAiSurfaceUsageHints } from '../core/ai-surface-inventory.js';
|
|
18
18
|
import { ensureUserStore, hasCompletedSetup } from '../core/setup-state.js';
|
|
19
|
+
import { resolveEmptyMemoryRecommendation } from '../core/setup-flow.js';
|
|
19
20
|
import { writeDetectedAgentExport } from './export.js';
|
|
20
21
|
import { writeDetectedAgentHooks } from './hooks.js';
|
|
22
|
+
import { checkGitPresence, runGlobalInstall } from './setup.js';
|
|
23
|
+
import { createBackup, BackupError } from '../core/upgrades/backup.js';
|
|
21
24
|
import { ConfigSchema } from '../core/schema.js';
|
|
22
25
|
export async function runInit(options = {}) {
|
|
23
26
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -29,6 +32,14 @@ export async function runInit(options = {}) {
|
|
|
29
32
|
if (!hasCompletedSetup()) {
|
|
30
33
|
ensureUserStore();
|
|
31
34
|
}
|
|
35
|
+
// Git-presence gate, aligned with `brainclaw setup`: agent-first onboarding
|
|
36
|
+
// assumes git for memory versioning + post-merge hooks. Allow override via
|
|
37
|
+
// BRAINCLAW_SKIP_REPO_ANALYSIS=1 for tests that exercise non-git fixtures.
|
|
38
|
+
if (process.env.BRAINCLAW_SKIP_REPO_ANALYSIS !== '1' && !checkGitPresence()) {
|
|
39
|
+
console.error('brainclaw init needs git to work.');
|
|
40
|
+
console.error('Install git from https://git-scm.com and try again.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
32
43
|
if (containingMemoryStore) {
|
|
33
44
|
console.error(`Error: cannot run \`brainclaw init\` from inside an existing project memory store (${containingMemoryStore}).`);
|
|
34
45
|
console.error('Run `brainclaw init` from the project root directory instead.');
|
|
@@ -61,6 +72,29 @@ export async function runInit(options = {}) {
|
|
|
61
72
|
const storageDir = resolveStorageDir(options.storageDir);
|
|
62
73
|
const projectMemoryExists = memoryExists(cwd);
|
|
63
74
|
const existingConfig = projectMemoryExists ? loadExistingConfig(cwd, storageDir) : undefined;
|
|
75
|
+
// --force backup gate: feedback_no_init_force (June 2026) entered the code.
|
|
76
|
+
// Before rebuilding identity fields on top of an existing store, take a
|
|
77
|
+
// sibling backup so curator personalisations (redaction patterns, claim
|
|
78
|
+
// TTL, governance, sensitive_paths) can always be recovered even if the
|
|
79
|
+
// merge below regresses or the agent ran `init --force` by mistake.
|
|
80
|
+
let forceBackupPath;
|
|
81
|
+
if (options.force && projectMemoryExists) {
|
|
82
|
+
try {
|
|
83
|
+
const handle = createBackup({
|
|
84
|
+
storePath: path.join(cwd, storageDir),
|
|
85
|
+
note: 'init --force pre-reconstruction snapshot',
|
|
86
|
+
storeSchemaVersion: existingConfig ? String(existingConfig.schema_version) : null,
|
|
87
|
+
});
|
|
88
|
+
forceBackupPath = handle.backupPath;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const reason = err instanceof BackupError
|
|
92
|
+
? `${err.code}: ${err.message}`
|
|
93
|
+
: err instanceof Error ? err.message : String(err);
|
|
94
|
+
console.error(`Error: --force backup failed (${reason}). Aborting to preserve the existing store. Re-run without --force, or move the store aside manually.`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
64
98
|
const topology = resolveTopology(options.topology, existingConfig?.topology);
|
|
65
99
|
const ignoreStrategy = resolveIgnoreStrategy(topology, existingConfig?.ignore_strategy);
|
|
66
100
|
const skipAgentBootstrap = options.skipAgentBootstrap === true || process.env.BRAINCLAW_SKIP_AGENT_BOOTSTRAP === '1';
|
|
@@ -87,7 +121,9 @@ export async function runInit(options = {}) {
|
|
|
87
121
|
? registerAgentIdentity({
|
|
88
122
|
agentName: detectedAi.name,
|
|
89
123
|
kind: detectedAi.kind,
|
|
90
|
-
|
|
124
|
+
// pln#562 step 2 — auto-registration never exceeds contributor;
|
|
125
|
+
// elevation is an explicit curator act (set-trust / register-agent).
|
|
126
|
+
trustLevel: 'contributor',
|
|
91
127
|
cwd,
|
|
92
128
|
preferredDirName: storageDir,
|
|
93
129
|
})
|
|
@@ -123,7 +159,15 @@ export async function runInit(options = {}) {
|
|
|
123
159
|
storageDir,
|
|
124
160
|
topology,
|
|
125
161
|
ignoreStrategy,
|
|
126
|
-
|
|
162
|
+
// --force rebuilds identity (project_id, agent, topology, storage_dir)
|
|
163
|
+
// but merges through existingConfig so curator personalisations
|
|
164
|
+
// (redaction patterns, governance, claims TTL, sensitive_paths,
|
|
165
|
+
// cross_project_links, custom markdown caps) survive the reset.
|
|
166
|
+
// The original `force ? undefined` path wiped these silently —
|
|
167
|
+
// discovered when feedback_no_init_force was promoted from a memory
|
|
168
|
+
// habit to a tracked regression.
|
|
169
|
+
existingConfig,
|
|
170
|
+
defaultJournalMode: !projectMemoryExists,
|
|
127
171
|
compact: options.compact === true,
|
|
128
172
|
});
|
|
129
173
|
if (detectedAi && isAgentIntegrationName(detectedAi.name)) {
|
|
@@ -141,6 +185,19 @@ export async function runInit(options = {}) {
|
|
|
141
185
|
.filter((hook) => hook.relativePath !== detectedExport?.relativePath))
|
|
142
186
|
: [];
|
|
143
187
|
const detectedAutoConfig = detectedAi ? writeDetectedAgentAutoConfig(detectedAi.name, cwd) : [];
|
|
188
|
+
// Per-agent slice of machine prerequisites (the same writes setup performs
|
|
189
|
+
// globally, but scoped to the detected agent). This makes `init` the single
|
|
190
|
+
// entry point for the carte-blanche / fresh-repo case: an agent-first
|
|
191
|
+
// bootstrap no longer needs a separate `brainclaw setup` shell-out + session
|
|
192
|
+
// reload. Idempotent — each ensure* function returns "skipped" when the
|
|
193
|
+
// agent's user-scope config doesn't exist.
|
|
194
|
+
const skipMachinePrereqs = options.skipMachinePrereqs === true
|
|
195
|
+
|| skipAgentBootstrap
|
|
196
|
+
|| testMode
|
|
197
|
+
|| process.env.BRAINCLAW_INIT_SKIP_MACHINE_PREREQS === '1';
|
|
198
|
+
const machinePrereqsWritten = detectedAi && !skipMachinePrereqs
|
|
199
|
+
? safeRunMachinePrereqs(detectedAi.name)
|
|
200
|
+
: [];
|
|
144
201
|
// Register in global project registry
|
|
145
202
|
try {
|
|
146
203
|
const entry = scanProject(cwd);
|
|
@@ -177,7 +234,10 @@ export async function runInit(options = {}) {
|
|
|
177
234
|
if (projectMemoryExists) {
|
|
178
235
|
console.log(`✔ Refreshed existing project memory in ${storageDir}/`);
|
|
179
236
|
if (options.force) {
|
|
180
|
-
|
|
237
|
+
if (forceBackupPath) {
|
|
238
|
+
console.log(`✔ Pre-reconstruction backup at ${forceBackupPath} (rollback: brainclaw upgrade --rollback)`);
|
|
239
|
+
}
|
|
240
|
+
console.log('✔ Existing memory preserved; rebuilt managed identity and refreshed agent integration files (customisations merged through)');
|
|
181
241
|
}
|
|
182
242
|
else {
|
|
183
243
|
console.log('✔ Existing memory preserved; refreshed managed configuration and agent integration files');
|
|
@@ -192,6 +252,12 @@ export async function runInit(options = {}) {
|
|
|
192
252
|
if (registeredAiAgent) {
|
|
193
253
|
console.log(`✔ AI agent detected: ${registeredAiAgent.agent_name} [${detectedAi.detection_source}] (${registeredAiAgent.agent_id})`);
|
|
194
254
|
}
|
|
255
|
+
if (machinePrereqsWritten.length > 0) {
|
|
256
|
+
console.log(`\u2714 Machine prerequisites for ${detectedAi.name}:`);
|
|
257
|
+
for (const filePath of machinePrereqsWritten) {
|
|
258
|
+
console.log(` - ${filePath}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
195
261
|
if (detectedExport) {
|
|
196
262
|
console.log(`\u2714 Agent instructions written to ${detectedExport.relativePath} (${detectedExport.created ? 'created' : 'updated'})`);
|
|
197
263
|
}
|
|
@@ -265,25 +331,36 @@ export async function runInit(options = {}) {
|
|
|
265
331
|
// Install post-merge hook for auto-release of claims after merge
|
|
266
332
|
installPostMergeHookIfMissing(cwd);
|
|
267
333
|
if (!testMode) {
|
|
268
|
-
|
|
334
|
+
// Shared empty-memory rule (see docs/concepts/workspace-bootstrapping.md):
|
|
335
|
+
// repo with content → bclaw_bootstrap extraction; greenfield → bootstrap
|
|
336
|
+
// loop. The brownfield preflight scan is skipped on greenfield — there is
|
|
337
|
+
// nothing to harvest yet.
|
|
338
|
+
const emptyMemoryRec = resolveEmptyMemoryRecommendation(cwd);
|
|
269
339
|
console.log('');
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
console.log(` ${line}`);
|
|
273
|
-
}
|
|
274
|
-
if (onboardingPreflight.importPlan.suggestion_count > 0) {
|
|
275
|
-
console.log('');
|
|
276
|
-
console.log(`Next step: run 'brainclaw bootstrap --apply' to import ${onboardingPreflight.importPlan.suggestion_count} suggested item(s) into canonical memory.`);
|
|
277
|
-
console.log(`Rollback: run 'brainclaw bootstrap --uninstall' to deactivate the last bootstrap-managed import.`);
|
|
278
|
-
}
|
|
279
|
-
if ((onboardingPreflight.importPlan.interview?.question_count ?? 0) > 0) {
|
|
280
|
-
console.log('');
|
|
281
|
-
console.log(`Interview: run 'brainclaw bootstrap --interview --audience cli' for terminal agents or '--audience ide_chat' for IDE chat agents.`);
|
|
282
|
-
console.log(`Apply confirmed answers: write a JSON answers file and run 'brainclaw bootstrap --answers-file <path> --apply'.`);
|
|
340
|
+
if (emptyMemoryRec.route === 'ideate') {
|
|
341
|
+
console.log(`Onboarding: ${emptyMemoryRec.text}`);
|
|
283
342
|
}
|
|
284
|
-
else
|
|
285
|
-
|
|
286
|
-
console.log(
|
|
343
|
+
else {
|
|
344
|
+
const onboardingPreflight = runBootstrapProfile({ cwd, refresh: true });
|
|
345
|
+
console.log('Onboarding preflight:');
|
|
346
|
+
console.log(` ${emptyMemoryRec.text}`);
|
|
347
|
+
for (const line of renderBootstrapSummary(onboardingPreflight).split('\n')) {
|
|
348
|
+
console.log(` ${line}`);
|
|
349
|
+
}
|
|
350
|
+
if (onboardingPreflight.importPlan.suggestion_count > 0) {
|
|
351
|
+
console.log('');
|
|
352
|
+
console.log(`Next step: run 'brainclaw bootstrap --apply' to import ${onboardingPreflight.importPlan.suggestion_count} suggested item(s) into canonical memory.`);
|
|
353
|
+
console.log(`Rollback: run 'brainclaw bootstrap --uninstall' to deactivate the last bootstrap-managed import.`);
|
|
354
|
+
}
|
|
355
|
+
if ((onboardingPreflight.importPlan.interview?.question_count ?? 0) > 0) {
|
|
356
|
+
console.log('');
|
|
357
|
+
console.log(`Interview: run 'brainclaw bootstrap --interview --audience cli' for terminal agents or '--audience ide_chat' for IDE chat agents.`);
|
|
358
|
+
console.log(`Apply confirmed answers: write a JSON answers file and run 'brainclaw bootstrap --answers-file <path> --apply'.`);
|
|
359
|
+
}
|
|
360
|
+
else if ((onboardingPreflight.profile.gaps?.length ?? 0) > 0) {
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log(`Next step: review the onboarding gaps, then use 'brainclaw bootstrap --json' as the basis for an interview/import flow.`);
|
|
363
|
+
}
|
|
287
364
|
}
|
|
288
365
|
}
|
|
289
366
|
console.log('');
|
|
@@ -293,7 +370,16 @@ export async function runInit(options = {}) {
|
|
|
293
370
|
else {
|
|
294
371
|
console.log(`Tip: run 'brainclaw init' again later to refresh the detected agent's integration files on this project.`);
|
|
295
372
|
}
|
|
296
|
-
console.log(`Tip:
|
|
373
|
+
console.log(`Tip: in an agent session, call the bclaw_work MCP tool (intent: "consult") to load the shared memory; from a terminal, 'brainclaw context --json' does the same.`);
|
|
374
|
+
}
|
|
375
|
+
function safeRunMachinePrereqs(agentName) {
|
|
376
|
+
try {
|
|
377
|
+
return runGlobalInstall([agentName]);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Non-fatal: machine-scope writes are best-effort, never block init.
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
297
383
|
}
|
|
298
384
|
function installPostMergeHookIfMissing(cwd) {
|
|
299
385
|
try {
|
|
@@ -483,6 +569,12 @@ function buildInitConfig(input) {
|
|
|
483
569
|
storageDir: input.storageDir,
|
|
484
570
|
topology: input.topology,
|
|
485
571
|
ignoreStrategy: input.ignoreStrategy,
|
|
572
|
+
// Solo-agent fresh default: the human running init is the default curator.
|
|
573
|
+
// Without it, approval_policy=review + curators=[] = every candidate sits
|
|
574
|
+
// in pending forever — a surprise the 2026-06-10 front-door audit flagged.
|
|
575
|
+
// mergeConfigWithDefaults preserves any explicit curators list on an
|
|
576
|
+
// existing store, so this only takes effect on fresh installs.
|
|
577
|
+
curatorName: input.currentAgent.name,
|
|
486
578
|
});
|
|
487
579
|
const config = input.existingConfig
|
|
488
580
|
? mergeConfigWithDefaults(input.existingConfig, fallbackConfig)
|
|
@@ -512,6 +604,16 @@ function buildInitConfig(input) {
|
|
|
512
604
|
max_items_per_section: Math.min(markdown.max_items_per_section, 20),
|
|
513
605
|
};
|
|
514
606
|
}
|
|
607
|
+
// pln#567 (decision A) — the event journal is ON by default for projects
|
|
608
|
+
// created through init. Set HERE, never in defaultConfig:
|
|
609
|
+
// createTestWorkspace builds its config straight from defaultConfig, so a dual
|
|
610
|
+
// default there would make the whole core suite dual-write (trp_65176454).
|
|
611
|
+
// Existing stores keep their current value, including unset legacy configs:
|
|
612
|
+
// `migrate --enable-journal` is the explicit path that turns them on and
|
|
613
|
+
// backfills genesis before future dual-writes depend on the journal.
|
|
614
|
+
if (input.defaultJournalMode === true && config.store?.journal?.mode === undefined) {
|
|
615
|
+
config.store = { ...config.store, journal: { ...config.store?.journal, mode: 'dual' } };
|
|
616
|
+
}
|
|
515
617
|
return config;
|
|
516
618
|
}
|
|
517
619
|
function mergeConfigWithDefaults(existingConfig, fallbackConfig) {
|
|
@@ -284,6 +284,7 @@ export function handleBclawLoop(options) {
|
|
|
284
284
|
input: req.input,
|
|
285
285
|
dispatch: req.dispatch,
|
|
286
286
|
assignment_id: req.assignment_id,
|
|
287
|
+
claim_id: req.claim_id,
|
|
287
288
|
actor,
|
|
288
289
|
}, options.cwd);
|
|
289
290
|
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
@@ -309,6 +310,9 @@ export function handleBclawLoop(options) {
|
|
|
309
310
|
: undefined,
|
|
310
311
|
actor,
|
|
311
312
|
caller_agent_id: req.agentId,
|
|
313
|
+
// pln#562 step 4 — a dispatched instance proves itself via its
|
|
314
|
+
// claim env; claim-bound slots reject same-named siblings.
|
|
315
|
+
caller_claim_id: process.env.BRAINCLAW_CLAIM_ID?.trim() || undefined,
|
|
312
316
|
}, options.cwd);
|
|
313
317
|
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
314
318
|
return successResponse('complete_turn', { loop, next_expected: computeNextExpected(loop) }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyBootstrapImport, renderBootstrapInterview, renderBootstrapSummary, runBootstrapProfile, uninstallBootstrapImport } from '../core/bootstrap.js';
|
|
2
2
|
import { buildAgentToolingContext, renderAgentToolingSummary } from '../core/agent-context.js';
|
|
3
|
-
import { buildCoordinationSnapshot } from '../core/coordination.js';
|
|
3
|
+
import { buildCoordinationSnapshot, buildCrossProjectSnapshot } from '../core/coordination.js';
|
|
4
4
|
import { scanDescendantPlans } from './list-plans.js';
|
|
5
5
|
import { buildContext } from '../core/context.js';
|
|
6
6
|
import { buildExecutionContext, renderExecutionContextSummary } from '../core/execution-context.js';
|
|
@@ -13,6 +13,7 @@ import { listClaims, assessClaimLiveness } from '../core/claims.js';
|
|
|
13
13
|
import { listAssignments } from '../core/assignments.js';
|
|
14
14
|
import { listAgentRuns } from '../core/agentruns.js';
|
|
15
15
|
import { reconcileAgentRun } from '../core/agentrun-reconciler.js';
|
|
16
|
+
import { isObserverMode } from '../core/observer-mode.js';
|
|
16
17
|
import { getDispatchStatus } from '../core/dispatch-status.js';
|
|
17
18
|
import { listActionRequired } from '../core/actions.js';
|
|
18
19
|
import { queryRuntimeEvents } from '../core/events.js';
|
|
@@ -35,6 +36,8 @@ import { listAvailableProjects, switchProject } from './switch.js';
|
|
|
35
36
|
import { resolveEffectiveCwd } from '../core/store-resolution.js';
|
|
36
37
|
import { resolveProjectCwd } from '../core/cross-project.js';
|
|
37
38
|
import { readUnseenEvents, buildNotificationSummary } from '../core/event-log.js';
|
|
39
|
+
import { boundListResult, DEFAULT_FIND_CHAR_BUDGET } from '../core/entity-operations.js';
|
|
40
|
+
import { handoffDiffPreviewNote } from '../core/handoff-snapshot.js';
|
|
38
41
|
import { BootstrapInterviewAnswerSchema, AssignmentStatusSchema, AgentRunStatusSchema, AgentRunTransportSchema, ActionRequiredStatusSchema, ActionRequiredKindSchema } from '../core/schema.js';
|
|
39
42
|
import { SCHEMA_VERSION, createToolErrorResponse, normaliseFormat, renderContextForMcp, } from './mcp.js';
|
|
40
43
|
function normalizeBootstrapInterviewAnswersArg(value) {
|
|
@@ -60,6 +63,33 @@ function normalizeBootstrapInterviewAudienceArg(value) {
|
|
|
60
63
|
}
|
|
61
64
|
return 'any';
|
|
62
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* trp#449 class (pln#542): bound an arbitrary object payload by repeatedly
|
|
68
|
+
* halving its largest array field until the serialized size fits the budget.
|
|
69
|
+
* Returns the bounded payload plus a per-field omitted count so the caller
|
|
70
|
+
* can advertise what was dropped.
|
|
71
|
+
*/
|
|
72
|
+
function boundObjectArrays(payload, charBudget) {
|
|
73
|
+
const omitted = {};
|
|
74
|
+
let current = { ...payload };
|
|
75
|
+
while (JSON.stringify(current).length > charBudget) {
|
|
76
|
+
let largestKey;
|
|
77
|
+
let largestLen = 1;
|
|
78
|
+
for (const [key, value] of Object.entries(current)) {
|
|
79
|
+
if (Array.isArray(value) && value.length > largestLen) {
|
|
80
|
+
largestKey = key;
|
|
81
|
+
largestLen = value.length;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!largestKey)
|
|
85
|
+
break;
|
|
86
|
+
const arr = current[largestKey];
|
|
87
|
+
const newLen = Math.max(1, Math.floor(arr.length / 2));
|
|
88
|
+
omitted[largestKey] = (omitted[largestKey] ?? 0) + (arr.length - newLen);
|
|
89
|
+
current = { ...current, [largestKey]: arr.slice(0, newLen) };
|
|
90
|
+
}
|
|
91
|
+
return { payload: current, omitted };
|
|
92
|
+
}
|
|
63
93
|
function getReviewAssignee(tags) {
|
|
64
94
|
for (const tag of tags) {
|
|
65
95
|
if (tag.startsWith('assignee:')) {
|
|
@@ -78,12 +108,19 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
78
108
|
// workspace store-chain children. Throws on unknown project — surfaces
|
|
79
109
|
// visibly as a tool error rather than silently falling back to the
|
|
80
110
|
// current project, which would mislead the caller.
|
|
111
|
+
// Precedence rule: once routing succeeds, per-handler "project as filter"
|
|
112
|
+
// logic is skipped (routing already scopes to the right store) — pln#359.
|
|
81
113
|
const projectArg = args.project;
|
|
82
114
|
const targetProjectArg = name === 'bclaw_switch' ? undefined : projectArg;
|
|
115
|
+
let projectRoutingApplied = false;
|
|
83
116
|
if (targetProjectArg) {
|
|
84
117
|
cwd = resolveProjectCwd(targetProjectArg, cwd);
|
|
118
|
+
projectRoutingApplied = true;
|
|
85
119
|
}
|
|
86
120
|
if (name === 'bclaw_get_context') {
|
|
121
|
+
// pln#542: budget_tokens caps the relevance-ranked fill (~4 chars/token).
|
|
122
|
+
// Explicit maxChars wins when both are given.
|
|
123
|
+
const budgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
87
124
|
const result = buildContext({
|
|
88
125
|
target: args.path,
|
|
89
126
|
project: targetProjectArg,
|
|
@@ -93,7 +130,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
93
130
|
profile: args.profile,
|
|
94
131
|
includePending: args.includePending,
|
|
95
132
|
maxItems: args.maxItems,
|
|
96
|
-
maxChars: args.maxChars,
|
|
133
|
+
maxChars: args.maxChars ?? (budgetTokens ? budgetTokens * 4 : undefined),
|
|
97
134
|
digest: args.digest,
|
|
98
135
|
sinceSession: args.since_session,
|
|
99
136
|
bootstrap: args.bootstrap,
|
|
@@ -133,10 +170,22 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
133
170
|
suggestions.push('\n💡 Tip: Use bclaw_get_capabilities, bclaw_list_tools, or bclaw_search_tools for detailed discovery');
|
|
134
171
|
enrichedContent = content + suggestions.join('\n');
|
|
135
172
|
}
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
173
|
+
// Unseen-event notifications. When buildContext already computed the
|
|
174
|
+
// event-cursor diff (the converged novelty mechanism, pln#542), reuse its
|
|
175
|
+
// histogram — the cursor was advanced by that read, so a second
|
|
176
|
+
// readUnseenEvents would observe nothing.
|
|
177
|
+
let notifications;
|
|
178
|
+
let unseenEventCount;
|
|
179
|
+
if (result.context_diff?.source === 'event_cursor') {
|
|
180
|
+
notifications = result.context_diff.event_summary;
|
|
181
|
+
unseenEventCount = result.context_diff.unseen_event_count;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const agentName = args.agent ?? resolveCurrentAgentName(cwd);
|
|
185
|
+
const unseenEvents = readUnseenEvents(agentName, cwd);
|
|
186
|
+
notifications = buildNotificationSummary(unseenEvents);
|
|
187
|
+
unseenEventCount = unseenEvents.length;
|
|
188
|
+
}
|
|
140
189
|
return {
|
|
141
190
|
content: [{ type: 'text', text: enrichedContent || 'No relevant memory found.' }],
|
|
142
191
|
structuredContent: {
|
|
@@ -151,7 +200,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
151
200
|
name: tool.name,
|
|
152
201
|
type: tool.type,
|
|
153
202
|
})),
|
|
154
|
-
...(notifications ? { pending_notifications: notifications, unseen_event_count:
|
|
203
|
+
...(notifications ? { pending_notifications: notifications, unseen_event_count: unseenEventCount } : {}),
|
|
155
204
|
},
|
|
156
205
|
};
|
|
157
206
|
}
|
|
@@ -197,12 +246,38 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
197
246
|
lines.push(`Suggestion: ${suggestion}`);
|
|
198
247
|
}
|
|
199
248
|
}
|
|
200
|
-
|
|
201
|
-
|
|
249
|
+
// trp#449 class (pln#542): the embedded git diff is unbounded — cap it so
|
|
250
|
+
// a large handoff snapshot never overflows the MCP token budget.
|
|
251
|
+
// budget_tokens tightens the cap (~4 chars/token).
|
|
252
|
+
const handoffBudgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
253
|
+
const diffCharBudget = handoffBudgetTokens ? Math.min(handoffBudgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
|
|
254
|
+
let boundedHandoff = handoff;
|
|
255
|
+
let diffTruncated = false;
|
|
256
|
+
if (handoff.snapshot?.diff && handoff.snapshot.diff.length > diffCharBudget) {
|
|
257
|
+
diffTruncated = true;
|
|
258
|
+
boundedHandoff = {
|
|
259
|
+
...handoff,
|
|
260
|
+
snapshot: {
|
|
261
|
+
...handoff.snapshot,
|
|
262
|
+
diff: `${handoff.snapshot.diff.slice(0, diffCharBudget)}\n… [diff truncated to ${diffCharBudget} chars — read the worktree branch for the full diff]`,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (boundedHandoff.snapshot?.diff) {
|
|
267
|
+
lines.push('', 'Uncommitted Git Diff:', boundedHandoff.snapshot.diff);
|
|
268
|
+
// pln#569 — the inline diff is a capped preview when a digest is present;
|
|
269
|
+
// tell the reader the full diff lives on the worktree branch.
|
|
270
|
+
const note = handoffDiffPreviewNote(boundedHandoff.snapshot);
|
|
271
|
+
if (!diffTruncated && note)
|
|
272
|
+
lines.push(note);
|
|
202
273
|
}
|
|
203
274
|
return {
|
|
204
275
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
205
|
-
structuredContent: {
|
|
276
|
+
structuredContent: {
|
|
277
|
+
handoff: boundedHandoff,
|
|
278
|
+
...(diffTruncated ? { diff_truncated: true } : {}),
|
|
279
|
+
schema_version: SCHEMA_VERSION,
|
|
280
|
+
},
|
|
206
281
|
};
|
|
207
282
|
}
|
|
208
283
|
if (name === 'bclaw_bootstrap') {
|
|
@@ -210,6 +285,23 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
210
285
|
if (args.apply && args.uninstall) {
|
|
211
286
|
throw new Error('bclaw_bootstrap does not allow apply and uninstall at the same time.');
|
|
212
287
|
}
|
|
288
|
+
// Mirror the CLI confirmAction gate: apply/uninstall mutate canonical
|
|
289
|
+
// memory, so they refuse without an explicit yes:true instead of relying
|
|
290
|
+
// on the host honouring the headlessApproval annotation.
|
|
291
|
+
if ((args.apply || args.uninstall) && args.yes !== true) {
|
|
292
|
+
const action = args.uninstall ? 'uninstall' : 'apply';
|
|
293
|
+
return {
|
|
294
|
+
content: [{
|
|
295
|
+
type: 'text',
|
|
296
|
+
text: `bclaw_bootstrap ${action} modifies canonical memory and requires explicit confirmation. Confirm with the user, then re-call with yes: true. No changes were made.`,
|
|
297
|
+
}],
|
|
298
|
+
structuredContent: {
|
|
299
|
+
confirmation_required: true,
|
|
300
|
+
action,
|
|
301
|
+
schema_version: SCHEMA_VERSION,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
213
305
|
if (args.uninstall) {
|
|
214
306
|
const result = uninstallBootstrapImport(cwd);
|
|
215
307
|
const text = !result.receipt
|
|
@@ -371,11 +463,34 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
371
463
|
return !plan || plan.status === 'todo';
|
|
372
464
|
}).length
|
|
373
465
|
: 0;
|
|
466
|
+
// pln#559 step 3 — attention_required is now the FULL composite the
|
|
467
|
+
// Attention section already displays in the extension: pending actions
|
|
468
|
+
// + non-auto (human-review) candidates + blocked assignments + stale
|
|
469
|
+
// runs. The 2026-06-10 calibration: the badge previously read only
|
|
470
|
+
// pendingActions and chronically undercounted while the Attention
|
|
471
|
+
// section showed N >> badge. The badge must NEVER be smaller than the
|
|
472
|
+
// section header it represents.
|
|
473
|
+
const pendingCandidates = listCandidates('pending', cwd);
|
|
474
|
+
const pendingHumanCandidates = pendingCandidates.filter((c) => resolvedSource(c) !== 'auto');
|
|
475
|
+
const allAssignments = listAssignments(cwd);
|
|
476
|
+
const blockedAssignments = allAssignments.filter((a) => a.status === 'blocked');
|
|
477
|
+
const allRuns = listAgentRuns(cwd);
|
|
478
|
+
const staleRuns = allRuns.filter((r) => r.status === 'blocked' || r.status === 'waiting_input' || r.status === 'failed');
|
|
479
|
+
const attentionRequiredComposite = pendingActions.length +
|
|
480
|
+
pendingHumanCandidates.length +
|
|
481
|
+
blockedAssignments.length +
|
|
482
|
+
staleRuns.length;
|
|
374
483
|
const summary = {
|
|
375
484
|
project_id: config.project_id,
|
|
376
485
|
agent,
|
|
377
486
|
current_host: currentHost,
|
|
378
|
-
attention_required:
|
|
487
|
+
attention_required: attentionRequiredComposite,
|
|
488
|
+
attention_breakdown: {
|
|
489
|
+
pending_actions: pendingActions.length,
|
|
490
|
+
pending_human_candidates: pendingHumanCandidates.length,
|
|
491
|
+
blocked_assignments: blockedAssignments.length,
|
|
492
|
+
stale_runs: staleRuns.length,
|
|
493
|
+
},
|
|
379
494
|
in_progress: activeClaims.length,
|
|
380
495
|
plans: {
|
|
381
496
|
in_progress: state.plan_items.filter((p) => p.status === 'in_progress').length,
|
|
@@ -491,9 +606,22 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
491
606
|
lines.push(`- ${other.name}: ${other.claim_count} claim(s) on ${other.scopes.join(', ')}`);
|
|
492
607
|
}
|
|
493
608
|
}
|
|
609
|
+
// trp#449 class (pln#542): the board aggregates many unbounded arrays —
|
|
610
|
+
// bound the structured payload by size. budget_tokens tightens the cap.
|
|
611
|
+
const boardBudgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
612
|
+
const boardCharBudget = boardBudgetTokens ? Math.min(boardBudgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
|
|
613
|
+
const { payload: boundedBoard, omitted } = boundObjectArrays(board, boardCharBudget);
|
|
494
614
|
return {
|
|
495
615
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
496
|
-
structuredContent: {
|
|
616
|
+
structuredContent: {
|
|
617
|
+
...boundedBoard,
|
|
618
|
+
...(Object.keys(omitted).length > 0
|
|
619
|
+
? {
|
|
620
|
+
omitted_for_size: omitted,
|
|
621
|
+
hint: `Payload size-bounded: ${Object.entries(omitted).map(([k, n]) => `${n} ${k}`).join(', ')} omitted. Use bclaw_find(entity=…) with filters to read the full lists.`,
|
|
622
|
+
}
|
|
623
|
+
: {}),
|
|
624
|
+
},
|
|
497
625
|
};
|
|
498
626
|
}
|
|
499
627
|
if (name === 'bclaw_search') {
|
|
@@ -512,10 +640,29 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
512
640
|
});
|
|
513
641
|
const total = allResults.length;
|
|
514
642
|
const page = allResults.slice(offset, offset + limit);
|
|
515
|
-
|
|
643
|
+
// trp#449 class — bound the page by size (pln#542). budget_tokens tightens
|
|
644
|
+
// the cap (~4 chars/token); the default mirrors bclaw_find's budget.
|
|
645
|
+
const budgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
646
|
+
const charBudget = budgetTokens ? Math.min(budgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
|
|
647
|
+
const bounded = boundListResult({ entity: 'search_result', total, items: page }, offset, charBudget);
|
|
648
|
+
const lines = bounded.items.map((result) => `[${result.id}] (${result.section}) score=${result.score.toFixed(2)}: ${result.text.slice(0, 120)}`);
|
|
649
|
+
const nextActions = bounded.has_more
|
|
650
|
+
? [{ tool: 'bclaw_search', args: { query, offset: bounded.next_offset, limit }, when: 'to fetch the next page' }]
|
|
651
|
+
: [];
|
|
516
652
|
return {
|
|
517
|
-
content: [{ type: 'text', text:
|
|
518
|
-
structuredContent: {
|
|
653
|
+
content: [{ type: 'text', text: bounded.items.length > 0 ? lines.join('\n') : 'No results found.' }],
|
|
654
|
+
structuredContent: {
|
|
655
|
+
total,
|
|
656
|
+
offset,
|
|
657
|
+
limit,
|
|
658
|
+
results: bounded.items,
|
|
659
|
+
returned: bounded.returned,
|
|
660
|
+
has_more: bounded.has_more,
|
|
661
|
+
...(bounded.next_offset !== undefined ? { next_offset: bounded.next_offset } : {}),
|
|
662
|
+
...(bounded.omitted_for_size ? { omitted_for_size: bounded.omitted_for_size } : {}),
|
|
663
|
+
...(bounded.hint ? { hint: bounded.hint } : {}),
|
|
664
|
+
next_actions: nextActions,
|
|
665
|
+
},
|
|
519
666
|
};
|
|
520
667
|
}
|
|
521
668
|
if (name === 'bclaw_estimation_report') {
|
|
@@ -563,7 +710,10 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
563
710
|
const assignee = String(args.assignee).toLowerCase();
|
|
564
711
|
plans = plans.filter((plan) => plan.assignee?.toLowerCase() === assignee);
|
|
565
712
|
}
|
|
566
|
-
if (args.project) {
|
|
713
|
+
if (args.project && !projectRoutingApplied) {
|
|
714
|
+
// Only filter by plan.project metadata when routing was NOT applied.
|
|
715
|
+
// When routing succeeded, cwd is already scoped to the target project store,
|
|
716
|
+
// so filtering by plan.project label would incorrectly narrow results.
|
|
567
717
|
const project = String(args.project).toLowerCase();
|
|
568
718
|
plans = plans.filter((plan) => plan.project?.toLowerCase() === project);
|
|
569
719
|
}
|
|
@@ -863,22 +1013,27 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
863
1013
|
// committed but never called bclaw_assignment_update) and surfaces
|
|
864
1014
|
// delivered_but_unverified for spawns past the 60s grace with no
|
|
865
1015
|
// life-sign — see runtime_note run_77e65e77 for the empirical case.
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
reconcileAgentRun(
|
|
1016
|
+
// Observer mode (BRAINCLAW_OBSERVER=1) suppresses this pre-read sweep —
|
|
1017
|
+
// a dashboard fetching assignment events must not be allowed to transition
|
|
1018
|
+
// agent_run records as a side effect of the read.
|
|
1019
|
+
if (!isObserverMode()) {
|
|
1020
|
+
try {
|
|
1021
|
+
if (runId) {
|
|
1022
|
+
reconcileAgentRun(runId, cwd);
|
|
873
1023
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1024
|
+
else if (assignmentId) {
|
|
1025
|
+
for (const run of listAgentRuns(cwd, { assignment_id: assignmentId })) {
|
|
1026
|
+
reconcileAgentRun(run.id, cwd);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
else if (claimId) {
|
|
1030
|
+
for (const run of listAgentRuns(cwd, { claim_id: claimId })) {
|
|
1031
|
+
reconcileAgentRun(run.id, cwd);
|
|
1032
|
+
}
|
|
878
1033
|
}
|
|
879
1034
|
}
|
|
1035
|
+
catch { /* defensive: never block events query on reconcile failure */ }
|
|
880
1036
|
}
|
|
881
|
-
catch { /* defensive: never block events query on reconcile failure */ }
|
|
882
1037
|
let events = queryRuntimeEvents({
|
|
883
1038
|
...(id ? { id } : {}),
|
|
884
1039
|
...(assignmentId ? { assignment_id: assignmentId } : {}),
|
|
@@ -1601,6 +1756,17 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
1601
1756
|
return handleMcpReadToolCall('bclaw_get_agent_board', args, context);
|
|
1602
1757
|
case 'board_summary':
|
|
1603
1758
|
return handleMcpReadToolCall('bclaw_get_agent_board_summary', args, context);
|
|
1759
|
+
case 'cross_project': {
|
|
1760
|
+
// pln#558 step 3 — lightweight endpoint for the VS Code extension's
|
|
1761
|
+
// SYSTEM section: returns linked_projects + incoming_signals only,
|
|
1762
|
+
// so the dashboard no longer pulls the full coordination snapshot
|
|
1763
|
+
// just to render two summary lists.
|
|
1764
|
+
const snap = buildCrossProjectSnapshot(cwd);
|
|
1765
|
+
return {
|
|
1766
|
+
content: [{ type: 'text', text: JSON.stringify(snap, null, 2) }],
|
|
1767
|
+
structuredContent: snap,
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1604
1770
|
case 'delta': {
|
|
1605
1771
|
const since = args.since;
|
|
1606
1772
|
if (typeof since !== 'string' || !since) {
|
|
@@ -1609,7 +1775,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
1609
1775
|
return handleMcpReadToolCall('bclaw_get_context', { ...args, since_session: since }, context);
|
|
1610
1776
|
}
|
|
1611
1777
|
default:
|
|
1612
|
-
throw new Error(`bclaw_context: unknown kind '${kind}'. Expected memory | execution | board | board_summary | delta.`);
|
|
1778
|
+
throw new Error(`bclaw_context: unknown kind '${kind}'. Expected memory | execution | board | board_summary | cross_project | delta.`);
|
|
1613
1779
|
}
|
|
1614
1780
|
}
|
|
1615
1781
|
if (name === 'bclaw_get_thread') {
|
|
@@ -1635,11 +1801,13 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
1635
1801
|
}
|
|
1636
1802
|
const tailLogLines = typeof args.tail_log_lines === 'number' ? args.tail_log_lines : undefined;
|
|
1637
1803
|
const stallThresholdMs = typeof args.stall_threshold_ms === 'number' ? args.stall_threshold_ms : undefined;
|
|
1804
|
+
const baseRef = typeof args.base_ref === 'string' && args.base_ref ? args.base_ref : undefined;
|
|
1638
1805
|
const status = getDispatchStatus({
|
|
1639
1806
|
target_id: targetId,
|
|
1640
1807
|
cwd,
|
|
1641
1808
|
tail_log_lines: tailLogLines,
|
|
1642
1809
|
stall_threshold_ms: stallThresholdMs,
|
|
1810
|
+
base_ref: baseRef,
|
|
1643
1811
|
});
|
|
1644
1812
|
// Text view: short, single-screen summary so an agent can decide what to do
|
|
1645
1813
|
// without parsing the structured payload.
|
|
@@ -1658,6 +1826,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
1658
1826
|
`Runtime: pid=${status.runtime.pid ?? '-'} alive=${status.runtime.pid_alive ?? 'unknown'} ack=${status.runtime.ack_file.exists}`,
|
|
1659
1827
|
` stdout: ${status.runtime.log_files.stdout?.exists ? `${status.runtime.log_files.stdout.size_bytes}B` : 'absent'}`,
|
|
1660
1828
|
` stderr: ${status.runtime.log_files.stderr?.exists ? `${status.runtime.log_files.stderr.size_bytes}B` : 'absent'}`,
|
|
1829
|
+
` git: commits_ahead=${status.runtime.commits_ahead ?? 'n/a'} dirty_tracked=${status.runtime.dirty_tracked ?? 'n/a'}`,
|
|
1661
1830
|
];
|
|
1662
1831
|
return {
|
|
1663
1832
|
content: [{ type: 'text', text: lines.join('\n') }],
|