brainclaw 1.8.0 → 1.9.1
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 +592 -505
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +138 -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 +286 -23
- package/dist/commands/hooks.js +73 -73
- package/dist/commands/init.js +124 -22
- package/dist/commands/install-hooks.js +78 -78
- package/dist/commands/loops-handlers.js +4 -0
- package/dist/commands/mcp-read-handlers.js +253 -41
- package/dist/commands/mcp.js +664 -102
- 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/switch.js +26 -5
- package/dist/commands/uninstall.js +126 -34
- package/dist/commands/update-step.js +6 -0
- package/dist/commands/version.js +1 -1
- package/dist/commands/worktree.js +60 -0
- package/dist/core/actions.js +12 -3
- package/dist/core/agent-capability.js +30 -17
- package/dist/core/agent-files.js +963 -666
- 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/codev-prompts.js +38 -38
- 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/default-profiles/doctor.yaml +11 -11
- package/dist/core/default-profiles/janitor.yaml +11 -11
- package/dist/core/default-profiles/onboarder.yaml +11 -11
- package/dist/core/default-profiles/reviewer.yaml +13 -13
- package/dist/core/dispatch-status.js +79 -5
- package/dist/core/dispatcher.js +65 -12
- package/dist/core/entity-operations.js +74 -27
- 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 +1 -1
- package/dist/core/facade-schema.js +38 -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 -2
- 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 +10 -3
- 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 +72 -10
- package/dist/core/runtime.js +84 -1
- package/dist/core/schema.js +114 -0
- package/dist/core/search.js +19 -2
- package/dist/core/security-detectors.js +125 -0
- package/dist/core/security-extract.js +189 -0
- package/dist/core/security-guard.js +217 -139
- 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 +16 -2
- package/dist/core/staleness.js +73 -2
- package/dist/core/state.js +250 -54
- package/dist/core/store-resolution.js +45 -12
- package/dist/core/worktree.js +90 -26
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/PROTOCOL.md +223 -0
- package/docs/adapters/openclaw.md +43 -43
- package/docs/architecture/project-refs.md +328 -328
- package/docs/cli.md +2097 -2096
- package/docs/concepts/coordination.md +52 -52
- package/docs/concepts/coordinator-runbook.md +129 -0
- package/docs/concepts/dispatch-lifecycle.md +245 -245
- package/docs/concepts/event-log-store.md +928 -0
- package/docs/concepts/ideation-loop.md +317 -317
- package/docs/concepts/loop-engine.md +520 -511
- package/docs/concepts/mcp-governance.md +268 -268
- package/docs/concepts/memory.md +89 -88
- package/docs/concepts/multi-agent-workflows.md +167 -167
- package/docs/concepts/observer-protocol.md +361 -0
- package/docs/concepts/parallel-merge-protocol.md +71 -0
- package/docs/concepts/plans-and-claims.md +217 -174
- package/docs/concepts/project-md-convention.md +35 -35
- package/docs/concepts/runtime-notes.md +38 -38
- package/docs/concepts/skills.md +78 -0
- package/docs/concepts/troubleshooting.md +254 -254
- package/docs/concepts/workspace-bootstrapping.md +142 -81
- package/docs/context-format-changelog.md +35 -35
- package/docs/context-format.md +48 -48
- package/docs/index.md +65 -65
- package/docs/integrations/agents.md +162 -162
- package/docs/integrations/claude-code.md +23 -23
- package/docs/integrations/cline.md +87 -88
- package/docs/integrations/codex.md +2 -2
- package/docs/integrations/continue.md +60 -60
- package/docs/integrations/copilot.md +82 -80
- package/docs/integrations/cursor.md +23 -23
- package/docs/integrations/kilocode.md +72 -72
- package/docs/integrations/mcp.md +377 -377
- package/docs/integrations/mistral-vibe.md +122 -122
- package/docs/integrations/openclaw.md +99 -98
- package/docs/integrations/opencode.md +84 -84
- package/docs/integrations/overview.md +122 -122
- package/docs/integrations/roo.md +74 -74
- package/docs/integrations/windsurf.md +83 -83
- package/docs/mcp-schema-changelog.md +360 -329
- package/docs/playbooks/integration/index.md +121 -121
- package/docs/playbooks/orchestration.md +37 -0
- package/docs/playbooks/productivity/index.md +99 -99
- package/docs/playbooks/team/index.md +117 -117
- package/docs/product/agent-first-model.md +184 -184
- package/docs/product/entity-model-audit.md +462 -462
- package/docs/product/positioning.md +86 -86
- package/docs/quickstart-existing-project.md +107 -107
- package/docs/quickstart.md +148 -147
- package/docs/release-maintenance.md +79 -79
- package/docs/reputation.md +52 -52
- package/docs/review.md +45 -45
- package/docs/security.md +212 -53
- package/docs/server-operations.md +118 -118
- package/docs/storage.md +110 -108
- package/package.json +86 -69
package/dist/commands/mcp.js
CHANGED
|
@@ -16,7 +16,8 @@ import { collectLoadValidationWarnings, findLoadValidationWarning, loadState, pe
|
|
|
16
16
|
import { generateIdWithLabel } from '../core/ids.js';
|
|
17
17
|
import { memoryExists } from '../core/io.js';
|
|
18
18
|
import { generateCandidateIdWithLabel, loadCandidate, saveCandidate } from '../core/candidates.js';
|
|
19
|
-
import { createEntity, getEntity, listEntities, boundListResult, removeEntity, transitionEntity, updateEntity, } from '../core/entity-operations.js';
|
|
19
|
+
import { createEntity, getEntity, listEntities, boundListResult, DEFAULT_FIND_CHAR_BUDGET, removeEntity, transitionEntity, updateEntity, } from '../core/entity-operations.js';
|
|
20
|
+
import { handoffDiffPreviewNote } from '../core/handoff-snapshot.js';
|
|
20
21
|
import { ENTITY_REGISTRY } from '../core/entity-registry.js';
|
|
21
22
|
import { generateClaimId, listClaims, loadClaim, saveClaim, createCoordinatorClaim, adoptClaimSession, attachAssignmentMessageToClaim, linkClaimToAssignment, releaseClaimWithCascade } from '../core/claims.js';
|
|
22
23
|
import { createSequence, updateSequence, deleteSequence } from '../core/sequence.js';
|
|
@@ -29,7 +30,7 @@ import { rejectCandidate } from './reject.js';
|
|
|
29
30
|
import { startSession } from './session-start.js';
|
|
30
31
|
import { endSession } from './session-end.js';
|
|
31
32
|
import { applyHandoffUpdates } from './update-handoff.js';
|
|
32
|
-
import { AgentIdentityResolutionError, AgentTrustError, findAgentIdentityById, findAgentIdentityByName, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveCurrentModel, ensureAgentRegisteredForDispatch, } from '../core/agent-registry.js';
|
|
33
|
+
import { AgentIdentityResolutionError, AgentTrustError, findAgentIdentityById, findAgentIdentityByName, hasMinimumTrustLevel, normalizeAgentName, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveCurrentAgentIdentity, resolveCurrentModel, ensureAgentRegisteredForDispatch, } from '../core/agent-registry.js';
|
|
33
34
|
import { appendAuditEntry } from '../core/audit.js';
|
|
34
35
|
import { nowISO, generateId } from '../core/ids.js';
|
|
35
36
|
import { buildOperationalIdentity, loadAllSessions, loadSessionById } from '../core/identity.js';
|
|
@@ -38,8 +39,8 @@ import { createCapability, createTool as createRegistryTool } from '../core/regi
|
|
|
38
39
|
import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
39
40
|
import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, getDetectedSetupAgentNames, getInstalledAgentNames, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
|
|
40
41
|
import { buildAgentInventory } from '../core/agent-inventory.js';
|
|
41
|
-
import { resolveEffectiveCwd, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
|
|
42
|
-
import { probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview } from '../core/setup-flow.js';
|
|
42
|
+
import { resolveEffectiveCwd, resolveEffectiveCwdInfo, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
|
|
43
|
+
import { assessBootstrapNeed, probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview, resolveEmptyMemoryRecommendation } from '../core/setup-flow.js';
|
|
43
44
|
import { ensureUserStore, resolveHomeDir } from '../core/setup-state.js';
|
|
44
45
|
import { createPlan, addStep as addStepOp, completeStep as completeStepOp, updateStep as updateStepOp, deleteStep as deleteStepOp, deletePlan as deletePlanOp } from '../core/operations/plan.js';
|
|
45
46
|
import { sendMessage, ackMessage, countActionable, getThread, hasActiveAssignment } from '../core/messaging.js';
|
|
@@ -95,8 +96,9 @@ export const MCP_READ_TOOLS = [
|
|
|
95
96
|
refresh: { type: 'boolean', description: 'Force a fresh bootstrap scan.' },
|
|
96
97
|
audience: { type: 'string', description: 'Optional interview audience filter: cli, ide_chat, or any.' },
|
|
97
98
|
interview: { type: 'boolean', description: 'Render interview text instead of the summary text.' },
|
|
98
|
-
apply: { type: 'boolean', description: 'Apply the current import proposal into canonical memory.' },
|
|
99
|
-
uninstall: { type: 'boolean', description: 'Uninstall the last bootstrap-managed import.' },
|
|
99
|
+
apply: { type: 'boolean', description: 'Apply the current import proposal into canonical memory. Requires yes: true.' },
|
|
100
|
+
uninstall: { type: 'boolean', description: 'Uninstall the last bootstrap-managed import. Requires yes: true.' },
|
|
101
|
+
yes: { type: 'boolean', description: 'Explicit confirmation for apply/uninstall (mirrors the CLI --yes gate). Without it the call returns confirmation_required and makes no changes.' },
|
|
100
102
|
interviewAnswers: {
|
|
101
103
|
type: 'array',
|
|
102
104
|
description: 'Optional structured interview answers. Each answer may include question_id, response_text, response_items, response_boolean, and explicit suggestions.',
|
|
@@ -119,15 +121,15 @@ export const MCP_READ_TOOLS = [
|
|
|
119
121
|
// Unified dispatcher over the four legacy context reads.
|
|
120
122
|
// Promoted to standard tier at the v1.0 cut.
|
|
121
123
|
name: 'bclaw_context',
|
|
122
|
-
description: 'Unified context read. Dispatches by kind: memory (project memory for a path), execution (local execution env), board (full agent board), board_summary (compact counts), delta (memory changes since a reference session).',
|
|
124
|
+
description: 'Unified context read. Dispatches by kind: memory (project memory for a path), execution (local execution env), board (full agent board), board_summary (compact counts), cross_project (linked_projects + incoming_signals only), delta (memory changes since a reference session).',
|
|
123
125
|
annotations: { tier: 'facade', category: 'context', headlessApproval: 'auto' },
|
|
124
126
|
inputSchema: {
|
|
125
127
|
type: 'object',
|
|
126
128
|
properties: {
|
|
127
129
|
kind: {
|
|
128
130
|
type: 'string',
|
|
129
|
-
enum: ['memory', 'execution', 'board', 'board_summary', 'delta'],
|
|
130
|
-
description: 'memory = project memory context; execution = local env/tooling; board = full agent board; board_summary = lightweight counts; delta = memory changes since `since`.',
|
|
131
|
+
enum: ['memory', 'execution', 'board', 'board_summary', 'cross_project', 'delta'],
|
|
132
|
+
description: 'memory = project memory context; execution = local env/tooling; board = full agent board; board_summary = lightweight counts; cross_project = linked_projects + incoming_signals only; delta = memory changes since `since`.',
|
|
131
133
|
},
|
|
132
134
|
since: {
|
|
133
135
|
type: 'string',
|
|
@@ -149,6 +151,7 @@ export const MCP_READ_TOOLS = [
|
|
|
149
151
|
compactTemplate: { type: 'boolean', description: 'Use compact template (memory kind, format=template).' },
|
|
150
152
|
includeAgentTooling: { type: 'boolean', description: 'Include agent tooling signals (execution kind).' },
|
|
151
153
|
project: { type: 'string', description: 'Optional: name of a linked project to read context from. Defaults to the current project. Accepts cross_project_links and workspace store-chain children.' },
|
|
154
|
+
budget_tokens: { type: 'number', description: 'Approximate token budget for the payload (~4 chars/token). memory kind: relevance-ranked item fill; board kind: arrays bounded by size.' },
|
|
152
155
|
},
|
|
153
156
|
required: ['kind'],
|
|
154
157
|
},
|
|
@@ -164,8 +167,11 @@ export const MCP_READ_TOOLS = [
|
|
|
164
167
|
type: { type: 'string', description: 'Filter by section: decisions, constraints, traps, handoffs, candidates, plans, sequences.' },
|
|
165
168
|
section: { type: 'string', description: 'Filter by section (state, candidates, runtime).' },
|
|
166
169
|
since: { type: 'string', description: 'Filter items created after this ISO date.' },
|
|
170
|
+
project: { type: 'string', description: 'Optional project name/path to search. Defaults to the active project.' },
|
|
171
|
+
includeLegacy: { type: 'boolean', description: 'Include records with provenance.kind="legacy" (default false). Response reports excluded_legacy when false.' },
|
|
167
172
|
limit: { type: 'number', description: 'Maximum number of results to return (default 10).' },
|
|
168
173
|
offset: { type: 'number', description: 'Number of results to skip (for pagination).' },
|
|
174
|
+
budget_tokens: { type: 'number', description: 'Optional token budget for the result page (~4 chars/token). The page is size-bounded; has_more/next_offset advertise the rest.' },
|
|
169
175
|
},
|
|
170
176
|
required: ['query'],
|
|
171
177
|
},
|
|
@@ -560,12 +566,13 @@ const MCP_WRITE_TOOLS = [
|
|
|
560
566
|
},
|
|
561
567
|
{
|
|
562
568
|
name: 'bclaw_quick_capture',
|
|
563
|
-
description: 'Capture free-form text
|
|
569
|
+
description: 'Capture free-form text as a decision, trap, constraint, or runtime note. Declare `type` yourself (you know what you are capturing — caller assertion wins); keyword heuristics are only a fallback when type is absent. Contradictions with existing memory are attached as advisory metadata on the candidate, never block promotion.',
|
|
564
570
|
annotations: { tier: 'standard', category: 'memory', headlessApproval: 'auto' },
|
|
565
571
|
inputSchema: {
|
|
566
572
|
type: 'object',
|
|
567
573
|
properties: {
|
|
568
574
|
text: { type: 'string', description: 'Free-form capture text.' },
|
|
575
|
+
type: { type: 'string', enum: ['decision', 'trap', 'constraint', 'note'], description: 'Caller-asserted classification. Strongly recommended — the calling agent knows the nature of the capture better than keyword heuristics (cnd_abe61d68: 18 false contradiction positives on a review summary).' },
|
|
569
576
|
context: { type: 'string', description: 'Optional file/path/scope context to associate with the capture.' },
|
|
570
577
|
agent: { type: 'string', description: 'Agent name.' },
|
|
571
578
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
@@ -643,7 +650,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
643
650
|
reflectHandoff: { type: 'boolean', description: 'Materialize an open handoff from git commits since session start.' },
|
|
644
651
|
dispatchReview: { type: 'boolean', description: 'When used with reflectHandoff, auto-dispatch a code review if the reflected handoff is reviewable.' },
|
|
645
652
|
reviewer: { type: 'string', description: 'Explicit reviewer for the reflected handoff review dispatch.' },
|
|
646
|
-
reflect: { type: 'boolean', description: '
|
|
653
|
+
reflect: { type: 'boolean', description: 'Emit the dogfooding reflection prompt (project + your surfaces/skills/tools). Default true — pass false to suppress on a trivial session. Capture actionable findings via bclaw_quick_capture.' },
|
|
647
654
|
},
|
|
648
655
|
},
|
|
649
656
|
},
|
|
@@ -701,6 +708,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
701
708
|
text: { type: 'string', description: 'Step description.' },
|
|
702
709
|
title: { type: 'string', description: 'Alias for text.' },
|
|
703
710
|
assignee: { type: 'string', description: 'Optional assignee.' },
|
|
711
|
+
estimated_effort: { type: 'number', description: 'Step-level estimate in minutes (pln#495). A duration string like "2h"/"30m" is also accepted and coerced.' },
|
|
712
|
+
actual_effort: { type: 'string', description: 'Step-level actual effort, free-form ("45m", "2h"), parsed when the estimation report runs.' },
|
|
704
713
|
},
|
|
705
714
|
},
|
|
706
715
|
text: { type: 'string', description: 'Legacy top-level step description; prefer data.text.' },
|
|
@@ -740,6 +749,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
740
749
|
status: { type: 'string', description: 'New status: todo, in_progress, testing, done, blocked.' },
|
|
741
750
|
text: { type: 'string', description: 'New step text.' },
|
|
742
751
|
assignee: { type: 'string', description: 'New assignee (empty string to unassign).' },
|
|
752
|
+
estimated_effort: { type: 'number', description: 'Step-level estimate in minutes (pln#495); a duration string is also coerced.' },
|
|
753
|
+
actual_effort: { type: 'string', description: 'Step-level actual effort, free-form ("45m", "2h").' },
|
|
743
754
|
agent: { type: 'string', description: 'Agent name.' },
|
|
744
755
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
745
756
|
project: { type: 'string', description: 'Optional: name of a linked project to update the step in. Defaults to the current project.' },
|
|
@@ -952,6 +963,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
952
963
|
agent: { type: 'string', description: 'Agent name.' },
|
|
953
964
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
954
965
|
compact: { type: 'boolean', description: 'Return a compact payload (default true). Set to false to include the full context result. Compact mode avoids exceeding MCP token limits on projects with large memory.', default: true },
|
|
966
|
+
budget_tokens: { type: 'number', description: 'Approximate token budget for the context payload. Relevance-ranked fill: highest-scoring items kept until the budget is reached (~4 chars/token).' },
|
|
955
967
|
},
|
|
956
968
|
required: ['intent'],
|
|
957
969
|
},
|
|
@@ -998,8 +1010,12 @@ const MCP_WRITE_TOOLS = [
|
|
|
998
1010
|
properties: {
|
|
999
1011
|
intent: {
|
|
1000
1012
|
type: 'string',
|
|
1001
|
-
|
|
1002
|
-
|
|
1013
|
+
// 'open' is intentionally NOT exposed standalone (pln#542): it
|
|
1014
|
+
// created a loop structure without dispatching the first turn, so
|
|
1015
|
+
// nothing ever ran. Loops are opened via
|
|
1016
|
+
// bclaw_coordinate(intent='review', open_loop=true) or intent='ideate'.
|
|
1017
|
+
enum: ['get', 'list', 'turn', 'complete_turn', 'advance', 'add_artifact', 'pause', 'resume', 'close'],
|
|
1018
|
+
description: 'Loop lifecycle intent for driving turns inside a loop that was already opened via the coordinate facade. To START a loop, use `bclaw_coordinate(intent="review", open_loop=true, targetAgents=[…])` or `intent="ideate"` — that opens the loop AND dispatches the first turn. See docs/concepts/loop-engine.md.',
|
|
1003
1019
|
},
|
|
1004
1020
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
1005
1021
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -1122,6 +1138,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1122
1138
|
entity: { type: 'string', description: 'Entity name: plan | decision | constraint | trap | handoff | runtime_note | candidate | sequence | claim | action | assignment | agent_run | cross_project_link. Others not yet wired.' },
|
|
1123
1139
|
filter: { type: 'object', description: 'Filter keys: status, tag (single tag), tags (array, any-match), author, plan_id, source, auto_generated, limit, offset, includeLegacy (bool, default false), minAutoReflectConfidence (0-1, default 0.6). entity=agent_run also accepts assignment_id, claim_id, message_id.' },
|
|
1124
1140
|
project: { type: 'string', description: 'Optional: name (or path/basename) of a linked project to query. Defaults to the current project. Only cross_project_links (config.yaml) and workspace store-chain children are accepted — list with `brainclaw link list`.' },
|
|
1141
|
+
budget_tokens: { type: 'number', description: 'Optional token budget for the page payload (~4 chars/token). Tightens the default size cap; pagination metadata (has_more/next_offset) still applies.' },
|
|
1125
1142
|
},
|
|
1126
1143
|
required: ['entity'],
|
|
1127
1144
|
},
|
|
@@ -1136,6 +1153,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1136
1153
|
entity: { type: 'string', description: 'Entity name.' },
|
|
1137
1154
|
id: { type: 'string', description: 'Entity id (e.g. dec_ab12cd) or short_label (e.g. dec#42).' },
|
|
1138
1155
|
project: { type: 'string', description: 'Optional: name of a linked project to fetch from. Defaults to the current project. See `brainclaw link list` for accepted names.' },
|
|
1156
|
+
budget_tokens: { type: 'number', description: 'Optional token budget (~4 chars/token). Bounds unbounded fields (e.g. handoff snapshot diffs).' },
|
|
1139
1157
|
},
|
|
1140
1158
|
required: ['entity', 'id'],
|
|
1141
1159
|
},
|
|
@@ -1226,6 +1244,39 @@ export const MCP_TOOL_NAMES = ALL_TOOLS.map((tool) => tool.name);
|
|
|
1226
1244
|
export const MCP_HEADLESS_AUTO_TOOL_NAMES = ALL_TOOLS
|
|
1227
1245
|
.filter((tool) => tool.annotations?.headlessApproval === 'auto')
|
|
1228
1246
|
.map((tool) => tool.name);
|
|
1247
|
+
/**
|
|
1248
|
+
* Narrow "canonical grammar" tool set — the read-side facade entries
|
|
1249
|
+
* (session + context) plus the five memory verbs (find / get / create /
|
|
1250
|
+
* update / transition). Consumed by writers (e.g. Hermes' tools.include)
|
|
1251
|
+
* that want a minimal advertised surface rather than the full headless-auto
|
|
1252
|
+
* catalog. Coordination facades (dispatch, coordinate, loop) are excluded
|
|
1253
|
+
* because narrow-surface agents shouldn't be routing work.
|
|
1254
|
+
*
|
|
1255
|
+
* Derivation rule (no hand-curated array):
|
|
1256
|
+
* - tier=facade AND category in {session, context} AND headlessApproval=auto
|
|
1257
|
+
* - OR name in the canonical memory verbs
|
|
1258
|
+
*
|
|
1259
|
+
* Adding a new memory grammar verb is the only edit that requires touching
|
|
1260
|
+
* this file; everything else propagates from ALL_TOOLS annotations (pln#546 step 2).
|
|
1261
|
+
*/
|
|
1262
|
+
const _CANONICAL_GRAMMAR_MEMORY_VERBS = new Set([
|
|
1263
|
+
'bclaw_find',
|
|
1264
|
+
'bclaw_get',
|
|
1265
|
+
'bclaw_create',
|
|
1266
|
+
'bclaw_update',
|
|
1267
|
+
'bclaw_transition',
|
|
1268
|
+
]);
|
|
1269
|
+
export const MCP_CANONICAL_GRAMMAR_TOOL_NAMES = ALL_TOOLS
|
|
1270
|
+
.filter((tool) => {
|
|
1271
|
+
const ann = tool.annotations ?? {};
|
|
1272
|
+
if (ann.tier === 'facade'
|
|
1273
|
+
&& (ann.category === 'session' || ann.category === 'context')
|
|
1274
|
+
&& ann.headlessApproval === 'auto') {
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
return _CANONICAL_GRAMMAR_MEMORY_VERBS.has(tool.name);
|
|
1278
|
+
})
|
|
1279
|
+
.map((tool) => tool.name);
|
|
1229
1280
|
/**
|
|
1230
1281
|
* Tools removed from the MCP surface at the v1.0 cut (Phase 3 slice 3i).
|
|
1231
1282
|
* Handlers remain in place defensively, but these names are hidden from
|
|
@@ -1333,6 +1384,21 @@ export const DEFAULT_PUBLISHED_TOOLS = PUBLISHED_TOOLS
|
|
|
1333
1384
|
return a.index - b.index;
|
|
1334
1385
|
})
|
|
1335
1386
|
.map(({ tool }) => tool);
|
|
1387
|
+
/**
|
|
1388
|
+
* Minimal catalog served while the project memory at cwd is absent.
|
|
1389
|
+
* Instead of refusing to boot (the historical exit(1)), the server starts
|
|
1390
|
+
* in "setup mode" so an agent landing on a fresh repo can initialize it
|
|
1391
|
+
* via bclaw_setup without a CLI shell-out + session-reload discontinuity.
|
|
1392
|
+
*/
|
|
1393
|
+
export const UNINITIALIZED_TOOL_NAMES = new Set([
|
|
1394
|
+
'bclaw_setup',
|
|
1395
|
+
'bclaw_init_project',
|
|
1396
|
+
'bclaw_doctor',
|
|
1397
|
+
]);
|
|
1398
|
+
export const UNINITIALIZED_PUBLISHED_TOOLS = PUBLISHED_TOOLS.filter((tool) => UNINITIALIZED_TOOL_NAMES.has(tool.name));
|
|
1399
|
+
export function buildUninitializedStateMessage(cwd) {
|
|
1400
|
+
return `Project memory not initialized at ${cwd}. The brainclaw MCP server is running in setup mode: only bclaw_setup, bclaw_init_project and bclaw_doctor are available. Call bclaw_setup to initialize this repo — the full tool catalog activates automatically afterwards.`;
|
|
1401
|
+
}
|
|
1336
1402
|
class McpProtocolError extends Error {
|
|
1337
1403
|
code;
|
|
1338
1404
|
id;
|
|
@@ -1525,10 +1591,108 @@ function getCancelledRequestId(params) {
|
|
|
1525
1591
|
}
|
|
1526
1592
|
return undefined;
|
|
1527
1593
|
}
|
|
1594
|
+
let principalCache;
|
|
1595
|
+
/** Test hook — the principal is otherwise pinned for the process lifetime. */
|
|
1596
|
+
export function __resetConnectionPrincipalForTests() {
|
|
1597
|
+
principalCache = undefined;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Resolve the connection principal. The MCP server is one process per
|
|
1601
|
+
* connection, so a process-level pin IS the per-connection pin; the cache key
|
|
1602
|
+
* guards the identity-bearing env vars so in-process test harnesses that
|
|
1603
|
+
* switch agents between calls re-resolve instead of leaking the first pin.
|
|
1604
|
+
*/
|
|
1605
|
+
function resolveConnectionPrincipal(cwd, sessionId) {
|
|
1606
|
+
const env = process.env;
|
|
1607
|
+
const key = [
|
|
1608
|
+
env.BRAINCLAW_CLAIM_ID ?? '', env.BRAINCLAW_AGENT_ID ?? '',
|
|
1609
|
+
env.BRAINCLAW_AGENT_NAME ?? '', env.BRAINCLAW_AGENT ?? '',
|
|
1610
|
+
cwd ?? '', sessionId ?? '',
|
|
1611
|
+
].join('|');
|
|
1612
|
+
if (principalCache && principalCache.key === key)
|
|
1613
|
+
return principalCache.value;
|
|
1614
|
+
let value;
|
|
1615
|
+
// 1. Assignment binding: a dispatched worker carries BRAINCLAW_CLAIM_ID; the
|
|
1616
|
+
// claim names the identity the coordinator dispatched — authoritative.
|
|
1617
|
+
const claimId = env.BRAINCLAW_CLAIM_ID?.trim();
|
|
1618
|
+
if (claimId) {
|
|
1619
|
+
try {
|
|
1620
|
+
const claim = loadClaim(claimId, cwd);
|
|
1621
|
+
const identity = (claim.agent_id ? findAgentIdentityById(claim.agent_id, cwd) : undefined)
|
|
1622
|
+
?? findAgentIdentityByName(claim.agent, cwd);
|
|
1623
|
+
if (identity) {
|
|
1624
|
+
value = {
|
|
1625
|
+
agent_name: identity.agent_name,
|
|
1626
|
+
agent_id: identity.agent_id,
|
|
1627
|
+
session_id: claim.session_id ?? sessionId,
|
|
1628
|
+
pid: process.pid,
|
|
1629
|
+
source: 'claim_binding',
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
catch { /* claim may not exist in this store — fall through */ }
|
|
1634
|
+
}
|
|
1635
|
+
// 2. Server-side detection (env-pinned or detected REGISTERED identity —
|
|
1636
|
+
// read-only since pln#562 step 2, never mints).
|
|
1637
|
+
if (!value) {
|
|
1638
|
+
const identity = resolveCurrentAgentIdentity(cwd);
|
|
1639
|
+
if (identity) {
|
|
1640
|
+
value = {
|
|
1641
|
+
agent_name: identity.agent_name,
|
|
1642
|
+
agent_id: identity.agent_id,
|
|
1643
|
+
session_id: sessionId,
|
|
1644
|
+
pid: process.pid,
|
|
1645
|
+
source: 'server_detection',
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
principalCache = { key, value };
|
|
1650
|
+
return value;
|
|
1651
|
+
}
|
|
1528
1652
|
function resolveMutationIdentity(args, fields, cwd, sessionId) {
|
|
1529
1653
|
try {
|
|
1654
|
+
const explicitName = typeof args[fields.nameField] === 'string' ? String(args[fields.nameField]) : undefined;
|
|
1655
|
+
const explicitId = typeof args[fields.idField] === 'string' ? String(args[fields.idField]) : undefined;
|
|
1656
|
+
// pln#562 step 3 — pinned connection principal. When the server resolved
|
|
1657
|
+
// an authenticated principal, caller args are verified against it:
|
|
1658
|
+
// matching/absent args → principal; mismatching args → curator-only
|
|
1659
|
+
// explicit override, otherwise the mismatch is REJECTED loudly. Silently
|
|
1660
|
+
// re-attributing a spoofed/mistaken identity to the principal would hide
|
|
1661
|
+
// caller bugs — fail-loud is the contract (mcp-protocol.test 'rejects
|
|
1662
|
+
// unregistered identities and mismatched id/name pairs').
|
|
1663
|
+
const principal = resolveConnectionPrincipal(cwd, sessionId);
|
|
1664
|
+
if (principal) {
|
|
1665
|
+
// Re-load per call (cheap) so trust changes propagate mid-connection;
|
|
1666
|
+
// the BINDING (who you are) stays pinned.
|
|
1667
|
+
const principalDoc = findAgentIdentityById(principal.agent_id, cwd);
|
|
1668
|
+
if (principalDoc) {
|
|
1669
|
+
const mismatch = (explicitName !== undefined && normalizeAgentName(explicitName) !== normalizeAgentName(principal.agent_name))
|
|
1670
|
+
|| (explicitId !== undefined && explicitId !== principal.agent_id);
|
|
1671
|
+
if (!mismatch) {
|
|
1672
|
+
return { identity: principalDoc };
|
|
1673
|
+
}
|
|
1674
|
+
if ((principalDoc.trust_level ?? 'contributor') === 'curator') {
|
|
1675
|
+
return {
|
|
1676
|
+
identity: requireRegisteredAgentIdentity({
|
|
1677
|
+
agentName: explicitName,
|
|
1678
|
+
agentId: explicitId,
|
|
1679
|
+
cwd,
|
|
1680
|
+
allowCurrent: true,
|
|
1681
|
+
allowEnv: true,
|
|
1682
|
+
}),
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
error: {
|
|
1687
|
+
kind: 'identity_error',
|
|
1688
|
+
message: `Caller-supplied identity (agent=${explicitName ?? '<none>'}, agentId=${explicitId ?? '<none>'}) does not match the pinned connection principal '${principal.agent_name}' (${principal.agent_id}). Omit the identity args, or have a curator perform the override.`,
|
|
1689
|
+
},
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
// No pinned principal (unregistered connection): legacy chain.
|
|
1530
1694
|
// Session-pinned identity: if no explicit agent in args, use the session's pinned agent
|
|
1531
|
-
let agentName =
|
|
1695
|
+
let agentName = explicitName;
|
|
1532
1696
|
if (!agentName && sessionId) {
|
|
1533
1697
|
const session = loadSessionById(sessionId, cwd);
|
|
1534
1698
|
if (session?.agent) {
|
|
@@ -1538,7 +1702,7 @@ function resolveMutationIdentity(args, fields, cwd, sessionId) {
|
|
|
1538
1702
|
return {
|
|
1539
1703
|
identity: requireRegisteredAgentIdentity({
|
|
1540
1704
|
agentName,
|
|
1541
|
-
agentId:
|
|
1705
|
+
agentId: explicitId,
|
|
1542
1706
|
cwd,
|
|
1543
1707
|
allowCurrent: true,
|
|
1544
1708
|
allowEnv: true,
|
|
@@ -1592,12 +1756,15 @@ function ensureTrust(args, fields, level, cwd, sessionId) {
|
|
|
1592
1756
|
}
|
|
1593
1757
|
/**
|
|
1594
1758
|
* Resolve the agent identity for canonical-grammar mutation verbs
|
|
1595
|
-
* (bclaw_create/update/remove/transition)
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
*
|
|
1759
|
+
* (bclaw_create/update/remove/transition), so handlers can auto-fill required
|
|
1760
|
+
* fields (e.g. plan.author) instead of letting the create land on disk with a
|
|
1761
|
+
* missing field — which would then be silently GC'd by the state sync loop
|
|
1762
|
+
* (see fix plan pln_5f44426c).
|
|
1599
1763
|
*
|
|
1600
|
-
*
|
|
1764
|
+
* pln#562 step 3 — resolution failure is a HARD error. The old fallback to
|
|
1765
|
+
* author:'unknown' produced records that passed creation but were schema-
|
|
1766
|
+
* invalid on read and silently GC'd: a write that lies about succeeding.
|
|
1767
|
+
* Callers map the throw to a validation_error tool response.
|
|
1601
1768
|
*/
|
|
1602
1769
|
function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
1603
1770
|
const resolved = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
@@ -1607,8 +1774,9 @@ function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
|
1607
1774
|
agent_id: resolved.identity.agent_id,
|
|
1608
1775
|
};
|
|
1609
1776
|
}
|
|
1610
|
-
const
|
|
1611
|
-
|
|
1777
|
+
const detail = 'error' in resolved && resolved.error ? resolved.error.message : 'no registered agent identity resolved';
|
|
1778
|
+
throw new Error(`cannot resolve mutation author: ${detail} `
|
|
1779
|
+
+ 'Start a session (bclaw_session_start) or pass a registered agent before writing.');
|
|
1612
1780
|
}
|
|
1613
1781
|
function explicitSessionIdFromEnv() {
|
|
1614
1782
|
return process.env.BRAINCLAW_SESSION_ID?.trim()
|
|
@@ -1616,6 +1784,32 @@ function explicitSessionIdFromEnv() {
|
|
|
1616
1784
|
|| process.env.CLAUDE_SESSION_ID?.trim()
|
|
1617
1785
|
|| process.env.COPILOT_SESSION_ID?.trim();
|
|
1618
1786
|
}
|
|
1787
|
+
function projectInfoForCwd(cwd) {
|
|
1788
|
+
try {
|
|
1789
|
+
const config = loadConfig(cwd);
|
|
1790
|
+
return { path: cwd, name: config.project_name };
|
|
1791
|
+
}
|
|
1792
|
+
catch {
|
|
1793
|
+
return { path: cwd };
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function scopeMetadataForTarget(args, targetCwd, effectiveScope) {
|
|
1797
|
+
const hasExplicitProject = typeof args.project === 'string' && args.project.trim().length > 0;
|
|
1798
|
+
return {
|
|
1799
|
+
resolved_project: projectInfoForCwd(targetCwd),
|
|
1800
|
+
active_source: hasExplicitProject ? 'explicit' : effectiveScope.active_source,
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function renderProvenanceFilterNote(result) {
|
|
1804
|
+
const parts = [];
|
|
1805
|
+
if ((result.excluded_legacy ?? 0) > 0) {
|
|
1806
|
+
parts.push(`${result.excluded_legacy} legacy item(s) excluded (pass filter.includeLegacy=true to include them)`);
|
|
1807
|
+
}
|
|
1808
|
+
if ((result.excluded_low_confidence_auto_reflect ?? 0) > 0) {
|
|
1809
|
+
parts.push(`${result.excluded_low_confidence_auto_reflect} low-confidence auto_reflect item(s) excluded (lower filter.minAutoReflectConfidence to include them)`);
|
|
1810
|
+
}
|
|
1811
|
+
return parts.length > 0 ? parts.join('; ') : undefined;
|
|
1812
|
+
}
|
|
1619
1813
|
export function parseMcpLine(line) {
|
|
1620
1814
|
let parsed;
|
|
1621
1815
|
try {
|
|
@@ -1650,11 +1844,17 @@ export function parseMcpLine(line) {
|
|
|
1650
1844
|
isNotification: !('id' in parsed),
|
|
1651
1845
|
};
|
|
1652
1846
|
}
|
|
1653
|
-
export function createInitializeResult(protocolVersion) {
|
|
1847
|
+
export function createInitializeResult(protocolVersion, options) {
|
|
1848
|
+
const uninitialized = options?.uninitialized === true;
|
|
1654
1849
|
return {
|
|
1655
1850
|
protocolVersion,
|
|
1656
1851
|
serverInfo: { name: 'brainclaw', version: SCHEMA_VERSION },
|
|
1657
|
-
|
|
1852
|
+
// listChanged is only advertised in setup mode, where the catalog flips
|
|
1853
|
+
// to the full set once the project memory is initialized.
|
|
1854
|
+
capabilities: { tools: { listChanged: uninitialized } },
|
|
1855
|
+
...(uninitialized
|
|
1856
|
+
? { instructions: buildUninitializedStateMessage(options?.cwd ?? process.cwd()) }
|
|
1857
|
+
: {}),
|
|
1658
1858
|
};
|
|
1659
1859
|
}
|
|
1660
1860
|
export class McpTaskRunner {
|
|
@@ -1777,6 +1977,8 @@ export class McpServerConnection {
|
|
|
1777
1977
|
state = 'pre_init';
|
|
1778
1978
|
protocolVersion;
|
|
1779
1979
|
connectionSessionId;
|
|
1980
|
+
/** True while the project memory at cwd is absent — serves the minimal setup catalog. */
|
|
1981
|
+
uninitializedMode;
|
|
1780
1982
|
/** Version of brainclaw code loaded in this process at boot time. */
|
|
1781
1983
|
bootVersion;
|
|
1782
1984
|
/** Throttle disk version checks — at most once per 60s. */
|
|
@@ -1787,11 +1989,15 @@ export class McpServerConnection {
|
|
|
1787
1989
|
constructor(options) {
|
|
1788
1990
|
this.cwd = options.cwd;
|
|
1789
1991
|
this.send = options.send;
|
|
1992
|
+
this.uninitializedMode = options.uninitialized ?? false;
|
|
1790
1993
|
this.bootVersion = getInstalledBrainclawVersion();
|
|
1791
1994
|
this.taskRunner = new McpTaskRunner({
|
|
1792
1995
|
executeTool: options.executeTool ?? createWorkerToolExecutor(),
|
|
1793
1996
|
onResult: (requestId, outcome) => {
|
|
1794
|
-
|
|
1997
|
+
if (outcome.nextConnectionSessionId !== undefined) {
|
|
1998
|
+
// null = explicit clear (session ended); string = refresh; undefined = no-op
|
|
1999
|
+
this.connectionSessionId = outcome.nextConnectionSessionId ?? undefined;
|
|
2000
|
+
}
|
|
1795
2001
|
// Inject version mismatch advisory if stale
|
|
1796
2002
|
const advisory = this.checkVersionMismatch();
|
|
1797
2003
|
if (advisory && outcome.response.content.length > 0) {
|
|
@@ -1800,17 +2006,43 @@ export class McpServerConnection {
|
|
|
1800
2006
|
...outcome.response.content,
|
|
1801
2007
|
];
|
|
1802
2008
|
}
|
|
2009
|
+
const catalogUnlocked = this.reconcileUninitializedMode();
|
|
2010
|
+
if (catalogUnlocked && outcome.response.content.length > 0) {
|
|
2011
|
+
outcome.response.content = [
|
|
2012
|
+
...outcome.response.content,
|
|
2013
|
+
{ type: 'text', text: '✔ Project memory initialized — the full brainclaw tool catalog is now active. If your client does not refresh tools automatically, reload the MCP server session.' },
|
|
2014
|
+
];
|
|
2015
|
+
}
|
|
1803
2016
|
// Track usage: append response size to usage.jsonl
|
|
1804
2017
|
if (outcome.toolName) {
|
|
1805
2018
|
this.trackUsage(outcome.toolName, outcome.response);
|
|
1806
2019
|
}
|
|
1807
2020
|
this.sendResult(requestId, outcome.response);
|
|
2021
|
+
if (catalogUnlocked) {
|
|
2022
|
+
this.send({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' });
|
|
2023
|
+
}
|
|
1808
2024
|
},
|
|
1809
2025
|
onInternalError: (requestId, error) => {
|
|
1810
2026
|
this.sendError(requestId, -32603, error instanceof Error ? error.message : 'Internal error');
|
|
1811
2027
|
},
|
|
1812
2028
|
});
|
|
1813
2029
|
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Lazy reconcile: if the server booted in setup mode but the project
|
|
2032
|
+
* memory now exists (initialized via bclaw_setup, bclaw_init_project,
|
|
2033
|
+
* or an out-of-band CLI init), unlock the full catalog.
|
|
2034
|
+
* Returns true exactly once — on the transition.
|
|
2035
|
+
*/
|
|
2036
|
+
reconcileUninitializedMode() {
|
|
2037
|
+
if (!this.uninitializedMode) {
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
if (!memoryExists(this.cwd)) {
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
2043
|
+
this.uninitializedMode = false;
|
|
2044
|
+
return true;
|
|
2045
|
+
}
|
|
1814
2046
|
/**
|
|
1815
2047
|
* Compare the version loaded in memory with the version on disk.
|
|
1816
2048
|
* Returns an advisory string if they differ, undefined otherwise.
|
|
@@ -1895,7 +2127,11 @@ export class McpServerConnection {
|
|
|
1895
2127
|
const protocolVersion = resolveRequestedProtocolVersion(params, id ?? null);
|
|
1896
2128
|
this.protocolVersion = protocolVersion;
|
|
1897
2129
|
this.state = 'awaiting_initialized';
|
|
1898
|
-
this.
|
|
2130
|
+
this.reconcileUninitializedMode();
|
|
2131
|
+
this.sendResult(id ?? null, createInitializeResult(protocolVersion, {
|
|
2132
|
+
uninitialized: this.uninitializedMode,
|
|
2133
|
+
cwd: this.cwd,
|
|
2134
|
+
}));
|
|
1899
2135
|
return;
|
|
1900
2136
|
}
|
|
1901
2137
|
if (method === 'notifications/initialized' || method === 'initialized') {
|
|
@@ -1918,6 +2154,15 @@ export class McpServerConnection {
|
|
|
1918
2154
|
}
|
|
1919
2155
|
if (method === 'tools/list') {
|
|
1920
2156
|
if (!isNotification) {
|
|
2157
|
+
this.reconcileUninitializedMode();
|
|
2158
|
+
if (this.uninitializedMode) {
|
|
2159
|
+
this.sendResult(id ?? null, {
|
|
2160
|
+
tools: UNINITIALIZED_PUBLISHED_TOOLS,
|
|
2161
|
+
uninitialized: true,
|
|
2162
|
+
state: buildUninitializedStateMessage(this.cwd),
|
|
2163
|
+
});
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
1921
2166
|
const params = message.params === undefined ? {} : requireObjectParams(message.params, id ?? null);
|
|
1922
2167
|
const catalog = typeof params.catalog === 'string' ? params.catalog : undefined;
|
|
1923
2168
|
const include = typeof params.include === 'string' ? params.include : undefined;
|
|
@@ -1947,6 +2192,19 @@ export class McpServerConnection {
|
|
|
1947
2192
|
return;
|
|
1948
2193
|
}
|
|
1949
2194
|
const args = params.arguments === undefined ? {} : requireObjectParams(params.arguments, id ?? null);
|
|
2195
|
+
this.reconcileUninitializedMode();
|
|
2196
|
+
if (this.uninitializedMode && !UNINITIALIZED_TOOL_NAMES.has(name)) {
|
|
2197
|
+
this.sendResult(id ?? null, toolResponse({
|
|
2198
|
+
content: [{ type: 'text', text: buildUninitializedStateMessage(this.cwd) }],
|
|
2199
|
+
structuredContent: {
|
|
2200
|
+
error: 'uninitialized',
|
|
2201
|
+
cwd: this.cwd,
|
|
2202
|
+
available_tools: [...UNINITIALIZED_TOOL_NAMES],
|
|
2203
|
+
next_action: 'Call bclaw_setup to initialize this repo.',
|
|
2204
|
+
},
|
|
2205
|
+
}, true));
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
1950
2208
|
this.taskRunner.enqueue(id ?? null, {
|
|
1951
2209
|
name,
|
|
1952
2210
|
args,
|
|
@@ -2082,12 +2340,15 @@ export class StdioTransport {
|
|
|
2082
2340
|
}
|
|
2083
2341
|
drainNewline() {
|
|
2084
2342
|
while (true) {
|
|
2085
|
-
|
|
2086
|
-
|
|
2343
|
+
// Search for '\n' (0x0a) at the byte level so a multibyte UTF-8 sequence
|
|
2344
|
+
// that spans two chunks is never split mid-character. Avoids O(n²)
|
|
2345
|
+
// string→buffer reconversion on every iteration.
|
|
2346
|
+
const newlineIndex = this.buffer.indexOf(0x0a);
|
|
2087
2347
|
if (newlineIndex === -1)
|
|
2088
2348
|
return;
|
|
2089
|
-
const
|
|
2090
|
-
this.buffer =
|
|
2349
|
+
const lineBuffer = this.buffer.subarray(0, newlineIndex);
|
|
2350
|
+
this.buffer = this.buffer.subarray(newlineIndex + 1);
|
|
2351
|
+
const line = lineBuffer.toString('utf-8').replace(/\r$/, '');
|
|
2091
2352
|
if (line.trim()) {
|
|
2092
2353
|
this.onMessage(line);
|
|
2093
2354
|
}
|
|
@@ -2096,9 +2357,12 @@ export class StdioTransport {
|
|
|
2096
2357
|
}
|
|
2097
2358
|
export function runMcp() {
|
|
2098
2359
|
const cwd = resolveEffectiveCwd();
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2360
|
+
// No project memory yet: start in setup mode instead of refusing to boot,
|
|
2361
|
+
// so agents can initialize the repo via bclaw_setup without a CLI
|
|
2362
|
+
// shell-out + session reload.
|
|
2363
|
+
const uninitialized = !memoryExists(cwd);
|
|
2364
|
+
if (uninitialized) {
|
|
2365
|
+
console.error(buildUninitializedStateMessage(cwd));
|
|
2102
2366
|
}
|
|
2103
2367
|
const missingWorkerPath = resolveMcpWorkerEntryPath();
|
|
2104
2368
|
if (!fs.existsSync(missingWorkerPath)) {
|
|
@@ -2120,6 +2384,7 @@ export function runMcp() {
|
|
|
2120
2384
|
cwd,
|
|
2121
2385
|
send: adaptiveSend,
|
|
2122
2386
|
executeTool: createWorkerToolExecutor(),
|
|
2387
|
+
uninitialized,
|
|
2123
2388
|
});
|
|
2124
2389
|
transport.onMessage = (line) => connection.handleLine(line);
|
|
2125
2390
|
transport.start();
|
|
@@ -2135,6 +2400,9 @@ function createWorkerToolExecutor() {
|
|
|
2135
2400
|
try {
|
|
2136
2401
|
worker = new Worker(new URL('./mcp-worker.js', import.meta.url), {
|
|
2137
2402
|
workerData: payload,
|
|
2403
|
+
// Capture worker stdout so console.log in tool handlers cannot corrupt
|
|
2404
|
+
// the parent's JSON-RPC stream. Drain captured output to stderr.
|
|
2405
|
+
stdout: true,
|
|
2138
2406
|
});
|
|
2139
2407
|
}
|
|
2140
2408
|
catch (error) {
|
|
@@ -2145,6 +2413,8 @@ function createWorkerToolExecutor() {
|
|
|
2145
2413
|
reject(error);
|
|
2146
2414
|
return;
|
|
2147
2415
|
}
|
|
2416
|
+
// Redirect any worker stdout to the server's stderr (safe for diagnostics)
|
|
2417
|
+
worker.stdout?.pipe(process.stderr);
|
|
2148
2418
|
let settled = false;
|
|
2149
2419
|
const cleanup = () => {
|
|
2150
2420
|
worker.removeAllListeners('message');
|
|
@@ -2295,7 +2565,10 @@ function getCrossProjectArg(args, ...keys) {
|
|
|
2295
2565
|
return undefined;
|
|
2296
2566
|
}
|
|
2297
2567
|
function blockCrossProjectExecution(entity, args) {
|
|
2298
|
-
|
|
2568
|
+
// Include 'project' so that canonical write verbs that accept a project routing
|
|
2569
|
+
// arg (e.g. bclaw_claim project=...) are subject to the same signaling-only
|
|
2570
|
+
// boundary as the explicit cross-project keys.
|
|
2571
|
+
const targetProject = getCrossProjectArg(args, 'targetProject', 'target_project', 'crossProject', 'cross_project', 'project');
|
|
2299
2572
|
if (!targetProject) {
|
|
2300
2573
|
return undefined;
|
|
2301
2574
|
}
|
|
@@ -2312,6 +2585,11 @@ import { handleMcpReadToolCall } from './mcp-read-handlers.js';
|
|
|
2312
2585
|
export { handleMcpReadToolCall };
|
|
2313
2586
|
async function _executeMcpToolCallInner(payload) {
|
|
2314
2587
|
const { name, args, cwd, connectionSessionId } = payload;
|
|
2588
|
+
const scopeInfo = payload.effectiveScope ?? {
|
|
2589
|
+
cwd,
|
|
2590
|
+
active_source: 'cwd',
|
|
2591
|
+
resolved_project: projectInfoForCwd(cwd),
|
|
2592
|
+
};
|
|
2315
2593
|
try {
|
|
2316
2594
|
if (isLegacyMcpToolFacadeDisabled(name)) {
|
|
2317
2595
|
return { response: createLegacyMcpToolDisabledResponse() };
|
|
@@ -2323,7 +2601,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2323
2601
|
}
|
|
2324
2602
|
if (MCP_READ_TOOLS.some((tool) => tool.name === name) || LEGACY_READ_TOOL_HANDLERS.has(name)) {
|
|
2325
2603
|
return {
|
|
2326
|
-
response: appendLegacyMcpToolWarning(toolResponse(handleMcpReadToolCall(name, args, { cwd })), name),
|
|
2604
|
+
response: appendLegacyMcpToolWarning(toolResponse(handleMcpReadToolCall(name, args, { cwd, connectionSessionId, effectiveScope: scopeInfo })), name),
|
|
2327
2605
|
};
|
|
2328
2606
|
}
|
|
2329
2607
|
// Resolve model once for all write operations
|
|
@@ -2385,20 +2663,24 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2385
2663
|
if (detected) {
|
|
2386
2664
|
summary.push(`✔ Agent detected: ${detected.name}`);
|
|
2387
2665
|
}
|
|
2388
|
-
summary.push('✔
|
|
2389
|
-
//
|
|
2666
|
+
summary.push('✔ Full brainclaw MCP catalog activates automatically; reload your agent session only if new tools do not appear.');
|
|
2667
|
+
// Bootstrap route follows the shared empty-memory rule; the preview
|
|
2668
|
+
// already embeds the same recommendation text when memory is empty.
|
|
2390
2669
|
const probe = probeForQuickSetup(cwd);
|
|
2391
2670
|
const bootstrapAvailable = probe.hasContent;
|
|
2671
|
+
const emptyMemoryRec = resolveEmptyMemoryRecommendation(cwd);
|
|
2392
2672
|
const preview = buildOnboardingPreview(cwd);
|
|
2393
2673
|
return {
|
|
2394
2674
|
response: toolResponse({
|
|
2395
|
-
content: [{ type: 'text', text: summary.join('\n') +
|
|
2675
|
+
content: [{ type: 'text', text: summary.join('\n') + '\n\n' + preview }],
|
|
2396
2676
|
structuredContent: {
|
|
2397
2677
|
setup_complete: true,
|
|
2398
2678
|
project_type: projectType,
|
|
2399
2679
|
topology,
|
|
2400
2680
|
detected_agent: detected?.name ?? null,
|
|
2401
2681
|
bootstrap_available: bootstrapAvailable,
|
|
2682
|
+
bootstrap_route: emptyMemoryRec.route,
|
|
2683
|
+
next_action: emptyMemoryRec.mcp_next_action,
|
|
2402
2684
|
preview,
|
|
2403
2685
|
summary,
|
|
2404
2686
|
},
|
|
@@ -2579,7 +2861,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2579
2861
|
});
|
|
2580
2862
|
const signal = writeCrossProjectSignal(resolveCrossProjectWritableTarget(crossProjectTarget, 'runtime_note', cwd), 'runtime_note', {
|
|
2581
2863
|
schema_version: 2,
|
|
2582
|
-
id: generateId('
|
|
2864
|
+
id: generateId('runtime_note'),
|
|
2583
2865
|
agent: opIdentity.agent,
|
|
2584
2866
|
agent_id: opIdentity.agent_id,
|
|
2585
2867
|
project_id: opIdentity.project_id ?? '',
|
|
@@ -2649,7 +2931,20 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2649
2931
|
}
|
|
2650
2932
|
}
|
|
2651
2933
|
const identity = resolved.identity;
|
|
2652
|
-
|
|
2934
|
+
// Caller-asserted classification wins (pln#542): the calling agent
|
|
2935
|
+
// declares decision/trap/constraint/note; keyword heuristics are the
|
|
2936
|
+
// fallback when no type is given.
|
|
2937
|
+
const assertedType = typeof args.type === 'string' ? args.type.trim().toLowerCase() : undefined;
|
|
2938
|
+
let classification;
|
|
2939
|
+
if (assertedType === 'decision' || assertedType === 'trap' || assertedType === 'constraint' || assertedType === 'note') {
|
|
2940
|
+
classification = { target: assertedType, reason: 'caller_asserted', decisionScore: 0, trapScore: 0 };
|
|
2941
|
+
}
|
|
2942
|
+
else if (assertedType !== undefined) {
|
|
2943
|
+
return { response: createToolErrorResponse('validation_error', `type must be one of: decision, trap, constraint, note (got '${assertedType}')`) };
|
|
2944
|
+
}
|
|
2945
|
+
else {
|
|
2946
|
+
classification = classifyQuickCapture(text);
|
|
2947
|
+
}
|
|
2653
2948
|
if (classification.target === 'note') {
|
|
2654
2949
|
const result = createRuntimeNote(formatQuickCaptureNoteText(text, context), {
|
|
2655
2950
|
agent: identity.agent_name,
|
|
@@ -2671,6 +2966,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2671
2966
|
note_id: result.noteId,
|
|
2672
2967
|
session_id: result.sessionId,
|
|
2673
2968
|
context,
|
|
2969
|
+
next_actions: [
|
|
2970
|
+
{ tool: 'bclaw_quick_capture', args: { text: '<same text>', type: 'decision' }, when: 'this was actually a durable decision/trap/constraint — re-capture with an asserted type' },
|
|
2971
|
+
],
|
|
2674
2972
|
},
|
|
2675
2973
|
}),
|
|
2676
2974
|
nextConnectionSessionId: explicitSessionIdFromEnv() ? undefined : result.sessionId,
|
|
@@ -2688,6 +2986,13 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2688
2986
|
const statusText = capture.writeThrough
|
|
2689
2987
|
? `✔ Quick capture promoted as ${classification.target} [${capture.promotedItemId}]`
|
|
2690
2988
|
: `✔ Quick capture saved as ${classification.target} candidate [${capture.candidateId}]`;
|
|
2989
|
+
const captureNextActions = capture.writeThrough
|
|
2990
|
+
? [
|
|
2991
|
+
{ tool: 'bclaw_get', args: { entity: classification.target, id: capture.promotedItemId }, when: 'to verify the promoted item' },
|
|
2992
|
+
]
|
|
2993
|
+
: [
|
|
2994
|
+
{ tool: 'bclaw_get', args: { entity: 'candidate', id: capture.candidateId }, when: 'to review the pending candidate (contradiction metadata is advisory)' },
|
|
2995
|
+
];
|
|
2691
2996
|
return {
|
|
2692
2997
|
response: toolResponse({
|
|
2693
2998
|
content: [{ type: 'text', text: statusText }],
|
|
@@ -2699,7 +3004,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2699
3004
|
candidate_id: capture.candidateId,
|
|
2700
3005
|
promoted_item_id: capture.promotedItemId,
|
|
2701
3006
|
write_through: capture.writeThrough,
|
|
2702
|
-
|
|
3007
|
+
// Advisory only (cnd_abe61d68): contradictions are metadata on
|
|
3008
|
+
// the candidate, never a promotion blocker.
|
|
2703
3009
|
contradiction_summary: capture.contradictionSummary,
|
|
2704
3010
|
contradictions_detected: capture.contradictionsDetected?.map((item) => ({
|
|
2705
3011
|
severity: item.severity,
|
|
@@ -2707,9 +3013,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2707
3013
|
conflicts_with: item.conflicts_with,
|
|
2708
3014
|
})),
|
|
2709
3015
|
context,
|
|
3016
|
+
next_actions: captureNextActions,
|
|
2710
3017
|
},
|
|
2711
3018
|
}),
|
|
2712
|
-
nextConnectionSessionId: undefined,
|
|
2713
3019
|
};
|
|
2714
3020
|
}
|
|
2715
3021
|
if (name === 'bclaw_create_plan') {
|
|
@@ -3038,10 +3344,29 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3038
3344
|
catch {
|
|
3039
3345
|
return { response: createToolErrorResponse('not_found', `Claim not found: ${claimId}`) };
|
|
3040
3346
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
});
|
|
3347
|
+
// pln#562 step 5 — release is ownership-checked like acquisition and
|
|
3348
|
+
// adoption. The resolved principal must own the claim; trusted+ callers
|
|
3349
|
+
// (coordinators) may release others' claims, with an audit entry.
|
|
3350
|
+
const releaseIdentity = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
3351
|
+
const releaseAuth = 'identity' in releaseIdentity && releaseIdentity.identity
|
|
3352
|
+
? {
|
|
3353
|
+
agent: releaseIdentity.identity.agent_name,
|
|
3354
|
+
agent_id: releaseIdentity.identity.agent_id,
|
|
3355
|
+
session_id: connectionSessionId,
|
|
3356
|
+
override: hasMinimumTrustLevel(releaseIdentity.identity.trust_level ?? 'contributor', 'trusted'),
|
|
3357
|
+
}
|
|
3358
|
+
: undefined;
|
|
3359
|
+
let cascadeResult;
|
|
3360
|
+
try {
|
|
3361
|
+
cascadeResult = releaseClaimWithCascade(claimId, {
|
|
3362
|
+
planStatus: args.planStatus,
|
|
3363
|
+
cwd,
|
|
3364
|
+
auth: releaseAuth,
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
catch (err) {
|
|
3368
|
+
return { response: createToolErrorResponse('trust_error', err instanceof Error ? err.message : String(err)) };
|
|
3369
|
+
}
|
|
3045
3370
|
const { planTransitioned, planWarning, planId: cascadePlanId, newPlanStatus: cascadeNewStatus } = cascadeResult;
|
|
3046
3371
|
const summaryText = [
|
|
3047
3372
|
`✔ Released claim [${claimId}]`,
|
|
@@ -3256,7 +3581,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3256
3581
|
...(result.handoff ? { handoff: result.handoff } : {}),
|
|
3257
3582
|
...(result.reflection_prompt ? { reflection_prompt: result.reflection_prompt } : {}),
|
|
3258
3583
|
}),
|
|
3259
|
-
nextConnectionSessionId:
|
|
3584
|
+
nextConnectionSessionId: null,
|
|
3260
3585
|
};
|
|
3261
3586
|
}
|
|
3262
3587
|
if (name === 'bclaw_compact') {
|
|
@@ -3390,20 +3715,41 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3390
3715
|
}
|
|
3391
3716
|
lines.push(` Sequence: ${analysis.sequence.name}`);
|
|
3392
3717
|
lines.push(` Ready: ${analysis.ready.length} | Active: ${analysis.active.length} | Blocked: ${analysis.blocked.length} | Done: ${analysis.done.length}`);
|
|
3718
|
+
// can_681a6c52 — truthful per-delivery status. The old output printed
|
|
3719
|
+
// '[inbox]' for every entry and ALWAYS dumped a 'Run these commands'
|
|
3720
|
+
// block, even when the auto-spawn had already SUCCEEDED — an obedient
|
|
3721
|
+
// coordinator would then double-spawn the worker.
|
|
3722
|
+
const spawnedPlanIds = new Set(dispatchResult.messages_sent
|
|
3723
|
+
.filter((m) => m.execution_status === 'delivered_and_started')
|
|
3724
|
+
.map((m) => m.plan_id));
|
|
3393
3725
|
if (dispatchResult.messages_sent.length > 0) {
|
|
3394
3726
|
lines.push('');
|
|
3395
3727
|
lines.push(args.dryRun ? ' Would assign:' : ' Assigned:');
|
|
3396
3728
|
for (const msg of dispatchResult.messages_sent) {
|
|
3397
3729
|
const lane = msg.lane ? ` (lane: ${msg.lane})` : '';
|
|
3398
|
-
|
|
3730
|
+
const exec = msg.execution_status ? ` [${msg.execution_status}]` : ' [inbox]';
|
|
3731
|
+
const pid = msg.pid ? ` pid=${msg.pid}` : '';
|
|
3732
|
+
const run = msg.run_id ? ` run=${msg.run_id}` : '';
|
|
3733
|
+
lines.push(` ${msg.agent}: ${msg.plan_id}${lane}${exec}${pid}${run}`);
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
const spawned = dispatchResult.messages_sent.filter((m) => m.execution_status === 'delivered_and_started');
|
|
3737
|
+
if (spawned.length > 0) {
|
|
3738
|
+
lines.push('');
|
|
3739
|
+
lines.push('Auto-spawn succeeded — do NOT run the launch commands for these (double-spawn risk). Verify instead:');
|
|
3740
|
+
for (const msg of spawned) {
|
|
3741
|
+
const target = msg.assignment_id ?? msg.run_id ?? msg.claim_id;
|
|
3742
|
+
lines.push(` bclaw_dispatch_status(target_id: "${target}") # ${msg.agent} on ${msg.plan_id}`);
|
|
3399
3743
|
}
|
|
3400
3744
|
}
|
|
3401
|
-
//
|
|
3402
|
-
|
|
3745
|
+
// Only surface manual launch commands for deliveries that were NOT
|
|
3746
|
+
// auto-spawned (manual fallback, spawn refusal, inbox-only).
|
|
3747
|
+
const manualCommands = dispatchResult.commands.filter((cmd) => !cmd.plan_id || !spawnedPlanIds.has(cmd.plan_id));
|
|
3748
|
+
if (manualCommands.length > 0) {
|
|
3403
3749
|
lines.push('');
|
|
3404
|
-
lines.push('Run these commands to launch the
|
|
3750
|
+
lines.push('Run these commands to launch the agents that were NOT auto-spawned:');
|
|
3405
3751
|
lines.push('');
|
|
3406
|
-
for (const cmd of
|
|
3752
|
+
for (const cmd of manualCommands) {
|
|
3407
3753
|
const lane = cmd.lane ? ` [lane: ${cmd.lane}]` : '';
|
|
3408
3754
|
lines.push(`# ${cmd.agent}${lane}`);
|
|
3409
3755
|
lines.push(cmd.command);
|
|
@@ -3417,6 +3763,15 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3417
3763
|
lines.push(` - ${skip.plan_id}: ${skip.reason}`);
|
|
3418
3764
|
}
|
|
3419
3765
|
}
|
|
3766
|
+
// can_45316d5c — worktree warnings / spawn refusals were hidden behind
|
|
3767
|
+
// dispatch_status; surface them in the cycle output itself.
|
|
3768
|
+
if (dispatchResult.warnings.length > 0) {
|
|
3769
|
+
lines.push('');
|
|
3770
|
+
lines.push(' Warnings:');
|
|
3771
|
+
for (const warning of dispatchResult.warnings) {
|
|
3772
|
+
lines.push(` ⚠ ${warning}`);
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3420
3775
|
appendAuditEntry({
|
|
3421
3776
|
actor: resolved.identity.agent_name,
|
|
3422
3777
|
actor_id: resolved.identity.agent_id,
|
|
@@ -3499,7 +3854,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3499
3854
|
if (status === 'accepted' && assignment.message_id) {
|
|
3500
3855
|
try {
|
|
3501
3856
|
const { ackMessage } = await import('../core/messaging.js');
|
|
3502
|
-
|
|
3857
|
+
// pln#562 step 4 — scope the ack to this assignment's claim so a
|
|
3858
|
+
// same-named sibling instance cannot consume the message.
|
|
3859
|
+
ackMessage(assignment.message_id, callerAgent, cwd, { claimId: assignment.claim_id });
|
|
3503
3860
|
}
|
|
3504
3861
|
catch { /* best-effort: don't fail the update if ack fails */ }
|
|
3505
3862
|
}
|
|
@@ -3666,7 +4023,11 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3666
4023
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
|
|
3667
4024
|
}
|
|
3668
4025
|
try {
|
|
3669
|
-
|
|
4026
|
+
// pln#562 step 4 — a dispatched instance (BRAINCLAW_CLAIM_ID) may only
|
|
4027
|
+
// ack messages bound to its own claim.
|
|
4028
|
+
const result = ackMessage(msgId, resolved.identity.agent_name, cwd, {
|
|
4029
|
+
claimId: process.env.BRAINCLAW_CLAIM_ID?.trim() || undefined,
|
|
4030
|
+
});
|
|
3670
4031
|
appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'update', item_id: result.id, item_type: 'message' }, cwd);
|
|
3671
4032
|
return {
|
|
3672
4033
|
response: toolResponse({
|
|
@@ -3768,13 +4129,15 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3768
4129
|
const stepTextRaw = stepData.text ?? stepData.title ?? args.text;
|
|
3769
4130
|
const stepText = typeof stepTextRaw === 'string' ? stepTextRaw.trim() : '';
|
|
3770
4131
|
const stepAssignee = (stepData.assignee ?? args.assignee);
|
|
4132
|
+
const stepEstimated = (stepData.estimated_effort ?? args.estimated_effort);
|
|
4133
|
+
const stepActual = (stepData.actual_effort ?? args.actual_effort);
|
|
3771
4134
|
if (!stepPlanId)
|
|
3772
4135
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
3773
4136
|
if (!stepText)
|
|
3774
4137
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: data.text') };
|
|
3775
4138
|
const stepTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3776
4139
|
try {
|
|
3777
|
-
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee: stepAssignee }, stepTargetCwd);
|
|
4140
|
+
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee: stepAssignee, estimatedEffort: stepEstimated, actualEffort: stepActual }, stepTargetCwd);
|
|
3778
4141
|
return {
|
|
3779
4142
|
response: toolResponse({
|
|
3780
4143
|
content: [{ type: 'text', text: `✔ Step added: [${result.stepId}] ${stepText} (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
@@ -3855,6 +4218,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3855
4218
|
status: args.status,
|
|
3856
4219
|
text: args.text,
|
|
3857
4220
|
assignee: args.assignee,
|
|
4221
|
+
estimatedEffort: args.estimated_effort,
|
|
4222
|
+
actualEffort: args.actual_effort,
|
|
3858
4223
|
}, usTargetCwd);
|
|
3859
4224
|
const changes = [];
|
|
3860
4225
|
if (args.status)
|
|
@@ -3863,6 +4228,10 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3863
4228
|
changes.push('text updated');
|
|
3864
4229
|
if (args.assignee !== undefined)
|
|
3865
4230
|
changes.push(`assignee=${args.assignee || 'unassigned'}`);
|
|
4231
|
+
if (args.estimated_effort !== undefined)
|
|
4232
|
+
changes.push(`estimate=${args.estimated_effort}`);
|
|
4233
|
+
if (args.actual_effort !== undefined)
|
|
4234
|
+
changes.push(`actual=${args.actual_effort}`);
|
|
3866
4235
|
return {
|
|
3867
4236
|
response: toolResponse({
|
|
3868
4237
|
content: [{ type: 'text', text: `✔ Step updated: [${result.stepId}] ${changes.join(', ')} (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
@@ -4290,11 +4659,11 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4290
4659
|
if (sessionResult.auto_registered) {
|
|
4291
4660
|
warnings.push(`Agent '${sessionResult.agent}' was auto-registered (first use). Run \`brainclaw register-agent ${sessionResult.agent}\` to set capabilities and trust level.`);
|
|
4292
4661
|
}
|
|
4293
|
-
// Step 2: build context for requested scope.
|
|
4294
|
-
//
|
|
4295
|
-
//
|
|
4296
|
-
//
|
|
4297
|
-
//
|
|
4662
|
+
// Step 2: build context for requested scope. The "what's new" diff is
|
|
4663
|
+
// surfaced for ALL intents (pln#390 regression fix): intent='resume'
|
|
4664
|
+
// anchors it on the agent's previous session; every other intent gets
|
|
4665
|
+
// the per-agent event-log-cursor diff computed inside buildContext
|
|
4666
|
+
// (pln#542 — converged novelty mechanism, covers status transitions).
|
|
4298
4667
|
let contextResult;
|
|
4299
4668
|
try {
|
|
4300
4669
|
let sinceSession;
|
|
@@ -4308,6 +4677,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4308
4677
|
agent: sessionResult.agent,
|
|
4309
4678
|
cwd: targetCwd,
|
|
4310
4679
|
sinceSession,
|
|
4680
|
+
// ~4 chars/token: relevance-ranked fill up to the caller's budget.
|
|
4681
|
+
maxChars: workReq.budget_tokens ? workReq.budget_tokens * 4 : undefined,
|
|
4311
4682
|
});
|
|
4312
4683
|
}
|
|
4313
4684
|
catch { /* non-fatal — context failure should not block work */ }
|
|
@@ -4368,11 +4739,30 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4368
4739
|
text: w.text.slice(0, 80),
|
|
4369
4740
|
age_days: w.age_days,
|
|
4370
4741
|
}));
|
|
4742
|
+
// pln#390 regression fix: the compact projection used to silently
|
|
4743
|
+
// drop context_diff, defeating the diff-first contract. Keep a
|
|
4744
|
+
// trimmed view (summary + counts + top-5 items, text capped).
|
|
4745
|
+
const trimmedDiff = contextResult.context_diff
|
|
4746
|
+
? {
|
|
4747
|
+
since: contextResult.context_diff.since,
|
|
4748
|
+
since_session: contextResult.context_diff.since_session,
|
|
4749
|
+
source: contextResult.context_diff.source,
|
|
4750
|
+
summary: contextResult.context_diff.summary,
|
|
4751
|
+
counts: contextResult.context_diff.counts,
|
|
4752
|
+
changed_items: (contextResult.context_diff.changed_items ?? [])
|
|
4753
|
+
.slice(0, 5)
|
|
4754
|
+
.map((item) => ({ ...item, text: item.text.slice(0, 120) })),
|
|
4755
|
+
...(contextResult.context_diff.unseen_event_count !== undefined
|
|
4756
|
+
? { unseen_event_count: contextResult.context_diff.unseen_event_count }
|
|
4757
|
+
: {}),
|
|
4758
|
+
}
|
|
4759
|
+
: undefined;
|
|
4371
4760
|
resultPayload = {
|
|
4372
4761
|
context_schema: contextResult.context_schema,
|
|
4373
4762
|
profile: contextResult.profile,
|
|
4374
4763
|
memory_version: contextResult.memory_version,
|
|
4375
4764
|
memory_density: contextResult.memory_density,
|
|
4765
|
+
context_diff: trimmedDiff ?? null,
|
|
4376
4766
|
plan_summary: planItems,
|
|
4377
4767
|
stale_warnings: staleTop3,
|
|
4378
4768
|
workflow_hints: (contextResult.workflow_hints ?? []).slice(0, 3),
|
|
@@ -4382,34 +4772,88 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4382
4772
|
_full_context_hint: 'Use bclaw_context(kind="memory") for the full payload.',
|
|
4383
4773
|
};
|
|
4384
4774
|
}
|
|
4385
|
-
// pln#513 step 1 — bootstrap hint.
|
|
4386
|
-
// PROJECT.md (
|
|
4387
|
-
//
|
|
4388
|
-
//
|
|
4389
|
-
//
|
|
4775
|
+
// pln#513 step 1 / pln#557 step 3 — bootstrap hint. The original probe
|
|
4776
|
+
// was a one-bit PROJECT.md stat(): false positive on a rich store
|
|
4777
|
+
// without PROJECT.md (recommended from-scratch bootstrap over 17k
|
|
4778
|
+
// events), eternal false negative on a fossil PROJECT.md. The composite
|
|
4779
|
+
// assessment (assessBootstrapNeed: presence × mtime-vs-activity ×
|
|
4780
|
+
// store density) adds a distinct 'refresh' verdict that maps to
|
|
4781
|
+
// bclaw_bootstrap(refresh: true) — coordinate with the pln#514 step 1
|
|
4782
|
+
// force-flag. 'bootstrap' keeps the shared empty-memory rule
|
|
4783
|
+
// (resolveEmptyMemoryRecommendation): repo with content → extract via
|
|
4784
|
+
// bclaw_bootstrap; greenfield → bootstrap loop. Both chainable.
|
|
4390
4785
|
let bootstrapRecommended;
|
|
4786
|
+
let bootstrapVerdict;
|
|
4787
|
+
let bootstrapRefreshReason;
|
|
4391
4788
|
let nextAction;
|
|
4789
|
+
let emptyMemoryRec;
|
|
4790
|
+
let storeDensity;
|
|
4392
4791
|
try {
|
|
4393
|
-
const
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
needsBootstrap = true;
|
|
4401
|
-
}
|
|
4402
|
-
catch {
|
|
4403
|
-
needsBootstrap = true; // ENOENT → absent
|
|
4792
|
+
const assessment = assessBootstrapNeed(targetCwd);
|
|
4793
|
+
bootstrapVerdict = assessment.verdict;
|
|
4794
|
+
bootstrapRecommended = assessment.verdict !== 'none';
|
|
4795
|
+
storeDensity = assessment.store_density;
|
|
4796
|
+
if (assessment.verdict === 'bootstrap') {
|
|
4797
|
+
emptyMemoryRec = resolveEmptyMemoryRecommendation(targetCwd);
|
|
4798
|
+
nextAction = emptyMemoryRec.mcp_next_action;
|
|
4404
4799
|
}
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
nextAction =
|
|
4800
|
+
else if (assessment.verdict === 'refresh') {
|
|
4801
|
+
bootstrapRefreshReason = assessment.reasons[0];
|
|
4802
|
+
nextAction = 'bclaw_bootstrap(refresh: true)';
|
|
4408
4803
|
}
|
|
4409
4804
|
}
|
|
4410
4805
|
catch {
|
|
4411
4806
|
// Best-effort: never block bclaw_work on the probe.
|
|
4412
4807
|
}
|
|
4808
|
+
// Self-teaching affordances (pln#542): each response carries the
|
|
4809
|
+
// recommended follow-up calls with exact shapes.
|
|
4810
|
+
const nextActions = [];
|
|
4811
|
+
if (bootstrapVerdict === 'bootstrap' && emptyMemoryRec) {
|
|
4812
|
+
if (emptyMemoryRec.route === 'extract') {
|
|
4813
|
+
nextActions.push({ tool: 'bclaw_bootstrap', args: {}, when: `project vision is missing and the ${emptyMemoryRec.reason} — extract initial context, then chain ${emptyMemoryRec.chained_mcp_action} if the vision is still missing` });
|
|
4814
|
+
}
|
|
4815
|
+
else {
|
|
4816
|
+
nextActions.push({ tool: 'bclaw_coordinate', args: { intent: 'ideate', preset: 'bootstrap' }, when: `project vision is missing and the repo is greenfield — open a bootstrap loop before assuming context, then chain ${emptyMemoryRec.chained_mcp_action} once content exists` });
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
else if (bootstrapVerdict === 'refresh') {
|
|
4820
|
+
nextActions.push({ tool: 'bclaw_bootstrap', args: { refresh: true }, when: bootstrapRefreshReason ?? 'PROJECT.md is missing or fossil relative to a mature store — refresh from existing memory, do not bootstrap from scratch' });
|
|
4821
|
+
}
|
|
4822
|
+
if (claimId) {
|
|
4823
|
+
nextActions.push({ tool: 'bclaw_release_claim', args: { id: claimId, planStatus: 'done' }, when: 'implementation complete and committed' });
|
|
4824
|
+
}
|
|
4825
|
+
else if (workReq.intent === 'consult' || workReq.intent === 'resume') {
|
|
4826
|
+
nextActions.push({ tool: 'bclaw_work', args: { intent: 'execute', scope: workReq.scope ?? '<scope>' }, when: 'ready to edit — claims the scope' });
|
|
4827
|
+
}
|
|
4828
|
+
// Solo-agent empty-store hint: the bootstrap recommendation covers
|
|
4829
|
+
// vision; agents arriving on a freshly-initialised store also need a
|
|
4830
|
+
// surface for *work* itself. Without this they reliably consult, see
|
|
4831
|
+
// nothing, and stop — bclaw_create(entity='plan') is the missing
|
|
4832
|
+
// affordance (2026-06-10 front-door audit). The store-density signal
|
|
4833
|
+
// bumps to 'low' as soon as session_start lands a single event, so
|
|
4834
|
+
// gate the hint directly on "no plans yet" — the actual condition
|
|
4835
|
+
// the agent is in.
|
|
4836
|
+
let noPlansYet = false;
|
|
4837
|
+
try {
|
|
4838
|
+
noPlansYet = loadState(targetCwd).plan_items.length === 0;
|
|
4839
|
+
}
|
|
4840
|
+
catch {
|
|
4841
|
+
// loadState may fail on a brand-new store with no project.md yet;
|
|
4842
|
+
// treat that as "no plans yet" — the next_action remains correct.
|
|
4843
|
+
noPlansYet = true;
|
|
4844
|
+
}
|
|
4845
|
+
if (noPlansYet && (storeDensity === 'empty' || storeDensity === 'low')) {
|
|
4846
|
+
nextActions.push({
|
|
4847
|
+
tool: 'bclaw_create',
|
|
4848
|
+
args: { entity: 'plan', input: { title: '<plan title>', steps: ['<first step>'] } },
|
|
4849
|
+
when: 'memory has no plans yet — once you know what you are doing, create a plan so progress is tracked',
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
const diffTotal = contextResult?.context_diff?.counts.total ?? 0;
|
|
4853
|
+
if (useCompact && diffTotal > 0) {
|
|
4854
|
+
nextActions.push({ tool: 'bclaw_context', args: { kind: 'memory' }, when: 'to read the full changed items behind context_diff' });
|
|
4855
|
+
}
|
|
4856
|
+
nextActions.push({ tool: 'bclaw_quick_capture', args: { text: '<finding>', type: '<decision|trap|constraint|note>' }, when: 'capture decisions/traps as you work' });
|
|
4413
4857
|
const facadeResponse = {
|
|
4414
4858
|
status: 'ok',
|
|
4415
4859
|
intent: workReq.intent,
|
|
@@ -4421,15 +4865,23 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4421
4865
|
warnings,
|
|
4422
4866
|
duration_ms: Date.now() - startMs,
|
|
4423
4867
|
bootstrap_recommended: bootstrapRecommended,
|
|
4868
|
+
bootstrap_verdict: bootstrapVerdict,
|
|
4424
4869
|
next_action: nextAction,
|
|
4870
|
+
next_actions: nextActions,
|
|
4425
4871
|
};
|
|
4426
4872
|
const summaryParts = [`✔ bclaw_work [${workReq.intent}] session=${sessionResult.session_id}`];
|
|
4427
4873
|
if (claimId)
|
|
4428
4874
|
summaryParts.push(`claim=${claimId} (${claimStatus})`);
|
|
4875
|
+
if (contextResult?.context_diff && diffTotal > 0) {
|
|
4876
|
+
summaryParts.push(`Δ since last look: ${contextResult.context_diff.summary}`);
|
|
4877
|
+
}
|
|
4429
4878
|
if (useCompact)
|
|
4430
4879
|
summaryParts.push('mode=compact (use bclaw_context for full payload)');
|
|
4431
|
-
if (
|
|
4432
|
-
summaryParts.push(`💡
|
|
4880
|
+
if (bootstrapVerdict === 'bootstrap' && emptyMemoryRec) {
|
|
4881
|
+
summaryParts.push(`💡 ${emptyMemoryRec.text}`);
|
|
4882
|
+
}
|
|
4883
|
+
else if (bootstrapVerdict === 'refresh') {
|
|
4884
|
+
summaryParts.push(`💡 ${bootstrapRefreshReason ?? 'PROJECT.md is missing or fossil relative to a mature store'} → bclaw_bootstrap(refresh: true)`);
|
|
4433
4885
|
}
|
|
4434
4886
|
if (warnings.length > 0)
|
|
4435
4887
|
summaryParts.push(warnings.map((w) => `⚠ ${w}`).join('\n'));
|
|
@@ -5732,6 +6184,23 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5732
6184
|
};
|
|
5733
6185
|
}
|
|
5734
6186
|
if (name === 'bclaw_loop') {
|
|
6187
|
+
// pln#542: intent='open' is no longer exposed standalone over MCP — it
|
|
6188
|
+
// creates a loop without dispatching the first turn (the documented
|
|
6189
|
+
// anti-pattern, now removed instead of documented). Internal callers
|
|
6190
|
+
// (bclaw_coordinate, CLI bootstrap) use core openLoop directly.
|
|
6191
|
+
if (args?.intent === 'open') {
|
|
6192
|
+
return {
|
|
6193
|
+
response: createToolErrorResponse('intent_not_exposed', "bclaw_loop(intent='open') is not exposed standalone: it creates a loop structure without dispatching any turn, so the work never starts. Use bclaw_coordinate(intent='review', open_loop=true, targetAgents=[…]) or bclaw_coordinate(intent='ideate') — they open the loop AND dispatch the first turn."),
|
|
6194
|
+
};
|
|
6195
|
+
}
|
|
6196
|
+
// pln#562 step 4 — dispatching a turn hands work to another agent; gate
|
|
6197
|
+
// it at the same trust bar as the other dispatch surfaces.
|
|
6198
|
+
if (args?.intent === 'turn' && args?.dispatch === true) {
|
|
6199
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
6200
|
+
if (resolved.error) {
|
|
6201
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
5735
6204
|
const { handleBclawLoop } = await import('./loops-handlers.js');
|
|
5736
6205
|
const targetCwd = resolveProjectCwd(args?.project, cwd);
|
|
5737
6206
|
const result = handleBclawLoop({ args: args, cwd: targetCwd });
|
|
@@ -5778,6 +6247,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5778
6247
|
try {
|
|
5779
6248
|
const entity = String(args.entity ?? '');
|
|
5780
6249
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6250
|
+
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
5781
6251
|
// pln#460 follow-up — some MCP clients (notably Claude Code with a
|
|
5782
6252
|
// tool schema that declares `filter: { type: 'object' }` without a
|
|
5783
6253
|
// sub-property schema) stringify the filter object before shipping
|
|
@@ -5825,9 +6295,19 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5825
6295
|
// this caps SIZE) so a verbose result set never overflows the MCP token
|
|
5826
6296
|
// cap and silently pushes the agent to the CLI (trp#449). Advertises
|
|
5827
6297
|
// has_more / next_offset / hint for explicit pagination.
|
|
6298
|
+
// pln#542: budget_tokens lets the caller shrink the size cap further
|
|
6299
|
+
// (~4 chars/token); it can only tighten, never exceed the default.
|
|
5828
6300
|
const offset = Math.max(0, Number(filter.offset) || 0);
|
|
5829
|
-
const
|
|
6301
|
+
const budgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
6302
|
+
const charBudget = budgetTokens ? Math.min(budgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
|
|
6303
|
+
const bounded = boundListResult(result, offset, charBudget);
|
|
5830
6304
|
const warnings = collectLoadValidationWarnings(entity, targetCwd);
|
|
6305
|
+
const nextActions = [
|
|
6306
|
+
{ 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' },
|
|
6307
|
+
];
|
|
6308
|
+
if (bounded.has_more) {
|
|
6309
|
+
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' });
|
|
6310
|
+
}
|
|
5831
6311
|
// structuredContent is the canonical MCP return channel that clients
|
|
5832
6312
|
// (VS Code extension, Codex, etc.) read for machine-parseable data.
|
|
5833
6313
|
// Prior to this fix we spread `...result` at top-level of the
|
|
@@ -5835,10 +6315,20 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5835
6315
|
// `result.items` arrived as undefined on the client — the root cause
|
|
5836
6316
|
// of the VS Code Backlog section rendering empty.
|
|
5837
6317
|
const moreNote = bounded.has_more ? ` (returned ${bounded.returned}; ${result.total - bounded.returned} more — offset ${bounded.next_offset})` : '';
|
|
6318
|
+
const provenanceFilterNote = renderProvenanceFilterNote(result);
|
|
6319
|
+
const legacyNote = provenanceFilterNote
|
|
6320
|
+
? `; ${provenanceFilterNote}`
|
|
6321
|
+
: '';
|
|
5838
6322
|
return {
|
|
5839
6323
|
response: toolResponse({
|
|
5840
|
-
content: [{ type: 'text', text: `✔ ${result.total} ${entity} item(s)${moreNote}` }],
|
|
5841
|
-
structuredContent: {
|
|
6324
|
+
content: [{ type: 'text', text: `✔ ${result.total} ${entity} item(s)${moreNote}${legacyNote}` }],
|
|
6325
|
+
structuredContent: {
|
|
6326
|
+
...bounded,
|
|
6327
|
+
warnings,
|
|
6328
|
+
resolved_project: targetScope.resolved_project,
|
|
6329
|
+
active_source: targetScope.active_source,
|
|
6330
|
+
next_actions: nextActions,
|
|
6331
|
+
},
|
|
5842
6332
|
}),
|
|
5843
6333
|
};
|
|
5844
6334
|
}
|
|
@@ -5867,10 +6357,35 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5867
6357
|
};
|
|
5868
6358
|
}
|
|
5869
6359
|
const item = getEntity(entity, id, targetCwd);
|
|
6360
|
+
// trp#449 class (pln#542): handoff snapshots embed an unbounded git
|
|
6361
|
+
// diff — cap it (budget_tokens tightens, ~4 chars/token).
|
|
6362
|
+
let boundedItem = item;
|
|
6363
|
+
let diffTruncated = false;
|
|
6364
|
+
const getBudgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
6365
|
+
const getCharBudget = getBudgetTokens ? Math.min(getBudgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
|
|
6366
|
+
if (entity === 'handoff' && item && typeof item === 'object') {
|
|
6367
|
+
const snapshot = item.snapshot;
|
|
6368
|
+
if (snapshot?.diff && snapshot.diff.length > getCharBudget) {
|
|
6369
|
+
diffTruncated = true;
|
|
6370
|
+
boundedItem = {
|
|
6371
|
+
...item,
|
|
6372
|
+
snapshot: { ...snapshot, diff: `${snapshot.diff.slice(0, getCharBudget)}\n… [diff truncated to ${getCharBudget} chars]` },
|
|
6373
|
+
};
|
|
6374
|
+
}
|
|
6375
|
+
else {
|
|
6376
|
+
const note = handoffDiffPreviewNote(snapshot);
|
|
6377
|
+
if (snapshot?.diff && note) {
|
|
6378
|
+
boundedItem = {
|
|
6379
|
+
...item,
|
|
6380
|
+
snapshot: { ...snapshot, diff: `${snapshot.diff}\n${note}` },
|
|
6381
|
+
};
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
5870
6385
|
return {
|
|
5871
6386
|
response: toolResponse({
|
|
5872
6387
|
content: [{ type: 'text', text: `✔ fetched ${entity} ${id}` }],
|
|
5873
|
-
structuredContent: { entity, item },
|
|
6388
|
+
structuredContent: { entity, item: boundedItem, ...(diffTruncated ? { diff_truncated: true } : {}) },
|
|
5874
6389
|
}),
|
|
5875
6390
|
};
|
|
5876
6391
|
}
|
|
@@ -5881,29 +6396,52 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5881
6396
|
if (name === 'bclaw_create') {
|
|
5882
6397
|
try {
|
|
5883
6398
|
const entity = String(args.entity ?? '');
|
|
6399
|
+
// Execution entities stay local: the signaling-only cross-project
|
|
6400
|
+
// boundary applies to the canonical verbs too, not just legacy tools.
|
|
6401
|
+
if (entity === 'claim' || entity === 'plan') {
|
|
6402
|
+
const crossProjectError = blockCrossProjectExecution(entity, args);
|
|
6403
|
+
if (crossProjectError)
|
|
6404
|
+
return { response: crossProjectError };
|
|
6405
|
+
}
|
|
5884
6406
|
const rawData = (args.data ?? {});
|
|
5885
6407
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6408
|
+
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
5886
6409
|
// Auto-fill identity fields. Without this, a caller who omits author/agent
|
|
5887
6410
|
// creates a schema-invalid record that is silently dropped on read and
|
|
5888
|
-
// GC'd from disk on the next mutation.
|
|
5889
|
-
// resolved MCP identity → args.agent → 'unknown'.
|
|
6411
|
+
// GC'd from disk on the next mutation.
|
|
5890
6412
|
// Identity is resolved against the SOURCE cwd (the agent's own
|
|
5891
6413
|
// project/registry), not the target — an agent doesn't need to be
|
|
5892
|
-
// registered in the target project to write into it.
|
|
5893
|
-
|
|
6414
|
+
// registered in the target project to write into it. An explicitly
|
|
6415
|
+
// supplied data.author is honored as content-level attribution
|
|
6416
|
+
// (cross-project signaling writers may not be registered locally);
|
|
6417
|
+
// when author is MISSING, resolution is mandatory and failure is a
|
|
6418
|
+
// hard validation_error (pln#562 step 3) — never author:'unknown'.
|
|
5894
6419
|
const data = { ...rawData };
|
|
5895
|
-
|
|
6420
|
+
let actor = typeof data.author === 'string' ? data.author : undefined;
|
|
6421
|
+
let actorId = typeof data.agent_id === 'string' ? data.agent_id : undefined;
|
|
6422
|
+
if (data.author === undefined) {
|
|
6423
|
+
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
5896
6424
|
data.author = agent_name;
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
6425
|
+
if (data.agent === undefined)
|
|
6426
|
+
data.agent = agent_name;
|
|
6427
|
+
if (data.agent_id === undefined && agent_id)
|
|
6428
|
+
data.agent_id = agent_id;
|
|
6429
|
+
actor = agent_name;
|
|
6430
|
+
actorId = agent_id;
|
|
6431
|
+
}
|
|
6432
|
+
else if (data.agent === undefined) {
|
|
6433
|
+
data.agent = data.author;
|
|
6434
|
+
}
|
|
5901
6435
|
const result = createEntity(entity, data, targetCwd);
|
|
5902
|
-
appendAuditEntry({ actor:
|
|
6436
|
+
appendAuditEntry({ actor: actor ?? 'unknown', ...(actorId ? { actor_id: actorId } : {}), action: 'create', item_id: result.id, item_type: entity }, targetCwd);
|
|
5903
6437
|
return {
|
|
5904
6438
|
response: toolResponse({
|
|
5905
6439
|
content: [{ type: 'text', text: `✔ created ${entity} ${result.id}` }],
|
|
5906
|
-
structuredContent: {
|
|
6440
|
+
structuredContent: {
|
|
6441
|
+
...result,
|
|
6442
|
+
resolved_project: targetScope.resolved_project,
|
|
6443
|
+
active_source: targetScope.active_source,
|
|
6444
|
+
},
|
|
5907
6445
|
}),
|
|
5908
6446
|
};
|
|
5909
6447
|
}
|
|
@@ -5917,13 +6455,18 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5917
6455
|
const id = String(args.id ?? '');
|
|
5918
6456
|
const patch = (args.patch ?? {});
|
|
5919
6457
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6458
|
+
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
5920
6459
|
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
5921
6460
|
const result = updateEntity(entity, id, patch, targetCwd);
|
|
5922
6461
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity }, targetCwd);
|
|
5923
6462
|
return {
|
|
5924
6463
|
response: toolResponse({
|
|
5925
6464
|
content: [{ type: 'text', text: `✔ updated ${entity} ${id}` }],
|
|
5926
|
-
structuredContent: {
|
|
6465
|
+
structuredContent: {
|
|
6466
|
+
...result,
|
|
6467
|
+
resolved_project: targetScope.resolved_project,
|
|
6468
|
+
active_source: targetScope.active_source,
|
|
6469
|
+
},
|
|
5927
6470
|
}),
|
|
5928
6471
|
};
|
|
5929
6472
|
}
|
|
@@ -5937,13 +6480,18 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5937
6480
|
const id = String(args.id ?? '');
|
|
5938
6481
|
const purge = args.purge === true;
|
|
5939
6482
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6483
|
+
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
5940
6484
|
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
5941
6485
|
const result = removeEntity(entity, id, targetCwd, purge);
|
|
5942
6486
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'delete', item_id: id, item_type: entity, reason: purge ? 'purged' : 'archived' }, targetCwd);
|
|
5943
6487
|
return {
|
|
5944
6488
|
response: toolResponse({
|
|
5945
6489
|
content: [{ type: 'text', text: `✔ removed ${entity} ${id}` }],
|
|
5946
|
-
structuredContent: {
|
|
6490
|
+
structuredContent: {
|
|
6491
|
+
...result,
|
|
6492
|
+
resolved_project: targetScope.resolved_project,
|
|
6493
|
+
active_source: targetScope.active_source,
|
|
6494
|
+
},
|
|
5947
6495
|
}),
|
|
5948
6496
|
};
|
|
5949
6497
|
}
|
|
@@ -5954,17 +6502,29 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5954
6502
|
if (name === 'bclaw_transition') {
|
|
5955
6503
|
try {
|
|
5956
6504
|
const entity = String(args.entity ?? '');
|
|
6505
|
+
// Same signaling-only boundary as bclaw_create: no remote lifecycle
|
|
6506
|
+
// driving of execution entities through the canonical grammar.
|
|
6507
|
+
if (entity === 'claim' || entity === 'plan') {
|
|
6508
|
+
const crossProjectError = blockCrossProjectExecution(entity, args);
|
|
6509
|
+
if (crossProjectError)
|
|
6510
|
+
return { response: crossProjectError };
|
|
6511
|
+
}
|
|
5957
6512
|
const id = String(args.id ?? '');
|
|
5958
6513
|
const to = String(args.to ?? '');
|
|
5959
6514
|
const reason = args.reason;
|
|
5960
6515
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6516
|
+
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
5961
6517
|
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
5962
6518
|
const result = transitionEntity(entity, id, to, targetCwd, reason);
|
|
5963
6519
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity, reason: `transition ${result.from} → ${to}${reason ? ` (${reason})` : ''}` }, targetCwd);
|
|
5964
6520
|
return {
|
|
5965
6521
|
response: toolResponse({
|
|
5966
6522
|
content: [{ type: 'text', text: `✔ ${entity} ${id}: ${result.from} → ${to}` }],
|
|
5967
|
-
structuredContent: {
|
|
6523
|
+
structuredContent: {
|
|
6524
|
+
...result,
|
|
6525
|
+
resolved_project: targetScope.resolved_project,
|
|
6526
|
+
active_source: targetScope.active_source,
|
|
6527
|
+
},
|
|
5968
6528
|
}),
|
|
5969
6529
|
};
|
|
5970
6530
|
}
|
|
@@ -6008,9 +6568,10 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6008
6568
|
*/
|
|
6009
6569
|
export async function executeMcpToolCall(payload) {
|
|
6010
6570
|
const baseCwd = payload.cwd;
|
|
6011
|
-
const
|
|
6012
|
-
? baseCwd
|
|
6013
|
-
:
|
|
6571
|
+
const effective = payload.name === 'bclaw_switch'
|
|
6572
|
+
? { cwd: baseCwd, active_source: 'cwd', resolved_project: undefined }
|
|
6573
|
+
: resolveEffectiveCwdInfo({ baseCwd, sessionId: payload.connectionSessionId });
|
|
6574
|
+
const cwd = effective.cwd;
|
|
6014
6575
|
const envClaimId = process.env.BRAINCLAW_CLAIM_ID?.trim() || undefined;
|
|
6015
6576
|
// ── Auto-session ────────────────────────────────────────────────────────────
|
|
6016
6577
|
let autoSessionId;
|
|
@@ -6068,6 +6629,7 @@ export async function executeMcpToolCall(payload) {
|
|
|
6068
6629
|
...payload,
|
|
6069
6630
|
cwd,
|
|
6070
6631
|
connectionSessionId: effectiveConnectionSessionId,
|
|
6632
|
+
effectiveScope: effective,
|
|
6071
6633
|
});
|
|
6072
6634
|
// Apply legacy deprecation warning uniformly (Phase 3 slice 3g). Read tools
|
|
6073
6635
|
// already get it at line 2560; write tools historically did not. This
|