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/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';
|
|
@@ -39,7 +40,7 @@ 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
42
|
import { resolveEffectiveCwd, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
|
|
42
|
-
import { probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview } from '../core/setup-flow.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
|
},
|
|
@@ -166,6 +169,7 @@ export const MCP_READ_TOOLS = [
|
|
|
166
169
|
since: { type: 'string', description: 'Filter items created after this ISO date.' },
|
|
167
170
|
limit: { type: 'number', description: 'Maximum number of results to return (default 10).' },
|
|
168
171
|
offset: { type: 'number', description: 'Number of results to skip (for pagination).' },
|
|
172
|
+
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
173
|
},
|
|
170
174
|
required: ['query'],
|
|
171
175
|
},
|
|
@@ -560,12 +564,13 @@ const MCP_WRITE_TOOLS = [
|
|
|
560
564
|
},
|
|
561
565
|
{
|
|
562
566
|
name: 'bclaw_quick_capture',
|
|
563
|
-
description: 'Capture free-form text
|
|
567
|
+
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
568
|
annotations: { tier: 'standard', category: 'memory', headlessApproval: 'auto' },
|
|
565
569
|
inputSchema: {
|
|
566
570
|
type: 'object',
|
|
567
571
|
properties: {
|
|
568
572
|
text: { type: 'string', description: 'Free-form capture text.' },
|
|
573
|
+
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
574
|
context: { type: 'string', description: 'Optional file/path/scope context to associate with the capture.' },
|
|
570
575
|
agent: { type: 'string', description: 'Agent name.' },
|
|
571
576
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
@@ -643,7 +648,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
643
648
|
reflectHandoff: { type: 'boolean', description: 'Materialize an open handoff from git commits since session start.' },
|
|
644
649
|
dispatchReview: { type: 'boolean', description: 'When used with reflectHandoff, auto-dispatch a code review if the reflected handoff is reviewable.' },
|
|
645
650
|
reviewer: { type: 'string', description: 'Explicit reviewer for the reflected handoff review dispatch.' },
|
|
646
|
-
reflect: { type: 'boolean', description: '
|
|
651
|
+
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
652
|
},
|
|
648
653
|
},
|
|
649
654
|
},
|
|
@@ -701,6 +706,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
701
706
|
text: { type: 'string', description: 'Step description.' },
|
|
702
707
|
title: { type: 'string', description: 'Alias for text.' },
|
|
703
708
|
assignee: { type: 'string', description: 'Optional assignee.' },
|
|
709
|
+
estimated_effort: { type: 'number', description: 'Step-level estimate in minutes (pln#495). A duration string like "2h"/"30m" is also accepted and coerced.' },
|
|
710
|
+
actual_effort: { type: 'string', description: 'Step-level actual effort, free-form ("45m", "2h"), parsed when the estimation report runs.' },
|
|
704
711
|
},
|
|
705
712
|
},
|
|
706
713
|
text: { type: 'string', description: 'Legacy top-level step description; prefer data.text.' },
|
|
@@ -740,6 +747,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
740
747
|
status: { type: 'string', description: 'New status: todo, in_progress, testing, done, blocked.' },
|
|
741
748
|
text: { type: 'string', description: 'New step text.' },
|
|
742
749
|
assignee: { type: 'string', description: 'New assignee (empty string to unassign).' },
|
|
750
|
+
estimated_effort: { type: 'number', description: 'Step-level estimate in minutes (pln#495); a duration string is also coerced.' },
|
|
751
|
+
actual_effort: { type: 'string', description: 'Step-level actual effort, free-form ("45m", "2h").' },
|
|
743
752
|
agent: { type: 'string', description: 'Agent name.' },
|
|
744
753
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
745
754
|
project: { type: 'string', description: 'Optional: name of a linked project to update the step in. Defaults to the current project.' },
|
|
@@ -952,6 +961,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
952
961
|
agent: { type: 'string', description: 'Agent name.' },
|
|
953
962
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
954
963
|
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 },
|
|
964
|
+
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
965
|
},
|
|
956
966
|
required: ['intent'],
|
|
957
967
|
},
|
|
@@ -972,6 +982,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
972
982
|
autoExecute: { type: 'boolean', description: 'Attempt to spawn target agents after delivery (default: true). When false, returns command_ready_manual with bash commands for the supervisor to run.' },
|
|
973
983
|
open_loop: { type: 'boolean', description: 'For intent=review only: also open a review Loop on top of the candidate (author + reviewer slots, advance to `findings`, dispatch turns). Default false — existing review callers are unaffected. See docs/concepts/loop-engine.md §Automation.' },
|
|
974
984
|
review_mode: { type: 'string', enum: ['asymmetric', 'symmetric'], description: 'Optional review Loop mode when open_loop=true. `asymmetric` (default) keeps the classical author→reviewer handoff; `symmetric` lets each reviewer turn also apply fixes directly, halving round-trips for spec/doc reviews. Ignored when open_loop is false.' },
|
|
985
|
+
preflight: { type: 'boolean', description: 'pln#533: when open_loop=true, run a trivial validation spawn per reviewer agent BEFORE opening the loop so an environment death (config rejected, auth fail, model mismatch) surfaces instantly with a clear reason instead of a generic loop timeout. Reviewers that fail pre-flight are dropped (with a targeted warning); if all fail, loop creation is skipped. Default true; set false to skip (e.g. you already ran `brainclaw doctor --spawn-check`). Ignored when open_loop is false or BRAINCLAW_NO_SPAWN is set.' },
|
|
975
986
|
agent: { type: 'string', description: 'Caller agent name.' },
|
|
976
987
|
agentId: { type: 'string', description: 'Caller registered agent id.' },
|
|
977
988
|
project: { type: 'string', description: 'Optional (pln#359 phase 1b): name of a linked project to dispatch into. When set, claim/assignment/message all land in the target project — the target agent picks the brief up async via its own bclaw_work. Auto-spawn is disabled in cross-project mode. Accepts cross_project_links and workspace store-chain children (see `brainclaw link list`).' },
|
|
@@ -997,8 +1008,12 @@ const MCP_WRITE_TOOLS = [
|
|
|
997
1008
|
properties: {
|
|
998
1009
|
intent: {
|
|
999
1010
|
type: 'string',
|
|
1000
|
-
|
|
1001
|
-
|
|
1011
|
+
// 'open' is intentionally NOT exposed standalone (pln#542): it
|
|
1012
|
+
// created a loop structure without dispatching the first turn, so
|
|
1013
|
+
// nothing ever ran. Loops are opened via
|
|
1014
|
+
// bclaw_coordinate(intent='review', open_loop=true) or intent='ideate'.
|
|
1015
|
+
enum: ['get', 'list', 'turn', 'complete_turn', 'advance', 'add_artifact', 'pause', 'resume', 'close'],
|
|
1016
|
+
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.',
|
|
1002
1017
|
},
|
|
1003
1018
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
1004
1019
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -1121,6 +1136,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1121
1136
|
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.' },
|
|
1122
1137
|
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.' },
|
|
1123
1138
|
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`.' },
|
|
1139
|
+
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.' },
|
|
1124
1140
|
},
|
|
1125
1141
|
required: ['entity'],
|
|
1126
1142
|
},
|
|
@@ -1135,6 +1151,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1135
1151
|
entity: { type: 'string', description: 'Entity name.' },
|
|
1136
1152
|
id: { type: 'string', description: 'Entity id (e.g. dec_ab12cd) or short_label (e.g. dec#42).' },
|
|
1137
1153
|
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.' },
|
|
1154
|
+
budget_tokens: { type: 'number', description: 'Optional token budget (~4 chars/token). Bounds unbounded fields (e.g. handoff snapshot diffs).' },
|
|
1138
1155
|
},
|
|
1139
1156
|
required: ['entity', 'id'],
|
|
1140
1157
|
},
|
|
@@ -1225,6 +1242,39 @@ export const MCP_TOOL_NAMES = ALL_TOOLS.map((tool) => tool.name);
|
|
|
1225
1242
|
export const MCP_HEADLESS_AUTO_TOOL_NAMES = ALL_TOOLS
|
|
1226
1243
|
.filter((tool) => tool.annotations?.headlessApproval === 'auto')
|
|
1227
1244
|
.map((tool) => tool.name);
|
|
1245
|
+
/**
|
|
1246
|
+
* Narrow "canonical grammar" tool set — the read-side facade entries
|
|
1247
|
+
* (session + context) plus the five memory verbs (find / get / create /
|
|
1248
|
+
* update / transition). Consumed by writers (e.g. Hermes' tools.include)
|
|
1249
|
+
* that want a minimal advertised surface rather than the full headless-auto
|
|
1250
|
+
* catalog. Coordination facades (dispatch, coordinate, loop) are excluded
|
|
1251
|
+
* because narrow-surface agents shouldn't be routing work.
|
|
1252
|
+
*
|
|
1253
|
+
* Derivation rule (no hand-curated array):
|
|
1254
|
+
* - tier=facade AND category in {session, context} AND headlessApproval=auto
|
|
1255
|
+
* - OR name in the canonical memory verbs
|
|
1256
|
+
*
|
|
1257
|
+
* Adding a new memory grammar verb is the only edit that requires touching
|
|
1258
|
+
* this file; everything else propagates from ALL_TOOLS annotations (pln#546 step 2).
|
|
1259
|
+
*/
|
|
1260
|
+
const _CANONICAL_GRAMMAR_MEMORY_VERBS = new Set([
|
|
1261
|
+
'bclaw_find',
|
|
1262
|
+
'bclaw_get',
|
|
1263
|
+
'bclaw_create',
|
|
1264
|
+
'bclaw_update',
|
|
1265
|
+
'bclaw_transition',
|
|
1266
|
+
]);
|
|
1267
|
+
export const MCP_CANONICAL_GRAMMAR_TOOL_NAMES = ALL_TOOLS
|
|
1268
|
+
.filter((tool) => {
|
|
1269
|
+
const ann = tool.annotations ?? {};
|
|
1270
|
+
if (ann.tier === 'facade'
|
|
1271
|
+
&& (ann.category === 'session' || ann.category === 'context')
|
|
1272
|
+
&& ann.headlessApproval === 'auto') {
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
return _CANONICAL_GRAMMAR_MEMORY_VERBS.has(tool.name);
|
|
1276
|
+
})
|
|
1277
|
+
.map((tool) => tool.name);
|
|
1228
1278
|
/**
|
|
1229
1279
|
* Tools removed from the MCP surface at the v1.0 cut (Phase 3 slice 3i).
|
|
1230
1280
|
* Handlers remain in place defensively, but these names are hidden from
|
|
@@ -1332,6 +1382,21 @@ export const DEFAULT_PUBLISHED_TOOLS = PUBLISHED_TOOLS
|
|
|
1332
1382
|
return a.index - b.index;
|
|
1333
1383
|
})
|
|
1334
1384
|
.map(({ tool }) => tool);
|
|
1385
|
+
/**
|
|
1386
|
+
* Minimal catalog served while the project memory at cwd is absent.
|
|
1387
|
+
* Instead of refusing to boot (the historical exit(1)), the server starts
|
|
1388
|
+
* in "setup mode" so an agent landing on a fresh repo can initialize it
|
|
1389
|
+
* via bclaw_setup without a CLI shell-out + session-reload discontinuity.
|
|
1390
|
+
*/
|
|
1391
|
+
export const UNINITIALIZED_TOOL_NAMES = new Set([
|
|
1392
|
+
'bclaw_setup',
|
|
1393
|
+
'bclaw_init_project',
|
|
1394
|
+
'bclaw_doctor',
|
|
1395
|
+
]);
|
|
1396
|
+
export const UNINITIALIZED_PUBLISHED_TOOLS = PUBLISHED_TOOLS.filter((tool) => UNINITIALIZED_TOOL_NAMES.has(tool.name));
|
|
1397
|
+
export function buildUninitializedStateMessage(cwd) {
|
|
1398
|
+
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.`;
|
|
1399
|
+
}
|
|
1335
1400
|
class McpProtocolError extends Error {
|
|
1336
1401
|
code;
|
|
1337
1402
|
id;
|
|
@@ -1524,10 +1589,108 @@ function getCancelledRequestId(params) {
|
|
|
1524
1589
|
}
|
|
1525
1590
|
return undefined;
|
|
1526
1591
|
}
|
|
1592
|
+
let principalCache;
|
|
1593
|
+
/** Test hook — the principal is otherwise pinned for the process lifetime. */
|
|
1594
|
+
export function __resetConnectionPrincipalForTests() {
|
|
1595
|
+
principalCache = undefined;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Resolve the connection principal. The MCP server is one process per
|
|
1599
|
+
* connection, so a process-level pin IS the per-connection pin; the cache key
|
|
1600
|
+
* guards the identity-bearing env vars so in-process test harnesses that
|
|
1601
|
+
* switch agents between calls re-resolve instead of leaking the first pin.
|
|
1602
|
+
*/
|
|
1603
|
+
function resolveConnectionPrincipal(cwd, sessionId) {
|
|
1604
|
+
const env = process.env;
|
|
1605
|
+
const key = [
|
|
1606
|
+
env.BRAINCLAW_CLAIM_ID ?? '', env.BRAINCLAW_AGENT_ID ?? '',
|
|
1607
|
+
env.BRAINCLAW_AGENT_NAME ?? '', env.BRAINCLAW_AGENT ?? '',
|
|
1608
|
+
cwd ?? '', sessionId ?? '',
|
|
1609
|
+
].join('|');
|
|
1610
|
+
if (principalCache && principalCache.key === key)
|
|
1611
|
+
return principalCache.value;
|
|
1612
|
+
let value;
|
|
1613
|
+
// 1. Assignment binding: a dispatched worker carries BRAINCLAW_CLAIM_ID; the
|
|
1614
|
+
// claim names the identity the coordinator dispatched — authoritative.
|
|
1615
|
+
const claimId = env.BRAINCLAW_CLAIM_ID?.trim();
|
|
1616
|
+
if (claimId) {
|
|
1617
|
+
try {
|
|
1618
|
+
const claim = loadClaim(claimId, cwd);
|
|
1619
|
+
const identity = (claim.agent_id ? findAgentIdentityById(claim.agent_id, cwd) : undefined)
|
|
1620
|
+
?? findAgentIdentityByName(claim.agent, cwd);
|
|
1621
|
+
if (identity) {
|
|
1622
|
+
value = {
|
|
1623
|
+
agent_name: identity.agent_name,
|
|
1624
|
+
agent_id: identity.agent_id,
|
|
1625
|
+
session_id: claim.session_id ?? sessionId,
|
|
1626
|
+
pid: process.pid,
|
|
1627
|
+
source: 'claim_binding',
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
catch { /* claim may not exist in this store — fall through */ }
|
|
1632
|
+
}
|
|
1633
|
+
// 2. Server-side detection (env-pinned or detected REGISTERED identity —
|
|
1634
|
+
// read-only since pln#562 step 2, never mints).
|
|
1635
|
+
if (!value) {
|
|
1636
|
+
const identity = resolveCurrentAgentIdentity(cwd);
|
|
1637
|
+
if (identity) {
|
|
1638
|
+
value = {
|
|
1639
|
+
agent_name: identity.agent_name,
|
|
1640
|
+
agent_id: identity.agent_id,
|
|
1641
|
+
session_id: sessionId,
|
|
1642
|
+
pid: process.pid,
|
|
1643
|
+
source: 'server_detection',
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
principalCache = { key, value };
|
|
1648
|
+
return value;
|
|
1649
|
+
}
|
|
1527
1650
|
function resolveMutationIdentity(args, fields, cwd, sessionId) {
|
|
1528
1651
|
try {
|
|
1652
|
+
const explicitName = typeof args[fields.nameField] === 'string' ? String(args[fields.nameField]) : undefined;
|
|
1653
|
+
const explicitId = typeof args[fields.idField] === 'string' ? String(args[fields.idField]) : undefined;
|
|
1654
|
+
// pln#562 step 3 — pinned connection principal. When the server resolved
|
|
1655
|
+
// an authenticated principal, caller args are verified against it:
|
|
1656
|
+
// matching/absent args → principal; mismatching args → curator-only
|
|
1657
|
+
// explicit override, otherwise the mismatch is REJECTED loudly. Silently
|
|
1658
|
+
// re-attributing a spoofed/mistaken identity to the principal would hide
|
|
1659
|
+
// caller bugs — fail-loud is the contract (mcp-protocol.test 'rejects
|
|
1660
|
+
// unregistered identities and mismatched id/name pairs').
|
|
1661
|
+
const principal = resolveConnectionPrincipal(cwd, sessionId);
|
|
1662
|
+
if (principal) {
|
|
1663
|
+
// Re-load per call (cheap) so trust changes propagate mid-connection;
|
|
1664
|
+
// the BINDING (who you are) stays pinned.
|
|
1665
|
+
const principalDoc = findAgentIdentityById(principal.agent_id, cwd);
|
|
1666
|
+
if (principalDoc) {
|
|
1667
|
+
const mismatch = (explicitName !== undefined && normalizeAgentName(explicitName) !== normalizeAgentName(principal.agent_name))
|
|
1668
|
+
|| (explicitId !== undefined && explicitId !== principal.agent_id);
|
|
1669
|
+
if (!mismatch) {
|
|
1670
|
+
return { identity: principalDoc };
|
|
1671
|
+
}
|
|
1672
|
+
if ((principalDoc.trust_level ?? 'contributor') === 'curator') {
|
|
1673
|
+
return {
|
|
1674
|
+
identity: requireRegisteredAgentIdentity({
|
|
1675
|
+
agentName: explicitName,
|
|
1676
|
+
agentId: explicitId,
|
|
1677
|
+
cwd,
|
|
1678
|
+
allowCurrent: true,
|
|
1679
|
+
allowEnv: true,
|
|
1680
|
+
}),
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
return {
|
|
1684
|
+
error: {
|
|
1685
|
+
kind: 'identity_error',
|
|
1686
|
+
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.`,
|
|
1687
|
+
},
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
// No pinned principal (unregistered connection): legacy chain.
|
|
1529
1692
|
// Session-pinned identity: if no explicit agent in args, use the session's pinned agent
|
|
1530
|
-
let agentName =
|
|
1693
|
+
let agentName = explicitName;
|
|
1531
1694
|
if (!agentName && sessionId) {
|
|
1532
1695
|
const session = loadSessionById(sessionId, cwd);
|
|
1533
1696
|
if (session?.agent) {
|
|
@@ -1537,7 +1700,7 @@ function resolveMutationIdentity(args, fields, cwd, sessionId) {
|
|
|
1537
1700
|
return {
|
|
1538
1701
|
identity: requireRegisteredAgentIdentity({
|
|
1539
1702
|
agentName,
|
|
1540
|
-
agentId:
|
|
1703
|
+
agentId: explicitId,
|
|
1541
1704
|
cwd,
|
|
1542
1705
|
allowCurrent: true,
|
|
1543
1706
|
allowEnv: true,
|
|
@@ -1591,12 +1754,15 @@ function ensureTrust(args, fields, level, cwd, sessionId) {
|
|
|
1591
1754
|
}
|
|
1592
1755
|
/**
|
|
1593
1756
|
* Resolve the agent identity for canonical-grammar mutation verbs
|
|
1594
|
-
* (bclaw_create/update/remove/transition)
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1757
|
+
* (bclaw_create/update/remove/transition), so handlers can auto-fill required
|
|
1758
|
+
* fields (e.g. plan.author) instead of letting the create land on disk with a
|
|
1759
|
+
* missing field — which would then be silently GC'd by the state sync loop
|
|
1760
|
+
* (see fix plan pln_5f44426c).
|
|
1598
1761
|
*
|
|
1599
|
-
*
|
|
1762
|
+
* pln#562 step 3 — resolution failure is a HARD error. The old fallback to
|
|
1763
|
+
* author:'unknown' produced records that passed creation but were schema-
|
|
1764
|
+
* invalid on read and silently GC'd: a write that lies about succeeding.
|
|
1765
|
+
* Callers map the throw to a validation_error tool response.
|
|
1600
1766
|
*/
|
|
1601
1767
|
function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
1602
1768
|
const resolved = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
@@ -1606,8 +1772,9 @@ function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
|
1606
1772
|
agent_id: resolved.identity.agent_id,
|
|
1607
1773
|
};
|
|
1608
1774
|
}
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1775
|
+
const detail = 'error' in resolved && resolved.error ? resolved.error.message : 'no registered agent identity resolved';
|
|
1776
|
+
throw new Error(`cannot resolve mutation author: ${detail} `
|
|
1777
|
+
+ 'Start a session (bclaw_session_start) or pass a registered agent before writing.');
|
|
1611
1778
|
}
|
|
1612
1779
|
function explicitSessionIdFromEnv() {
|
|
1613
1780
|
return process.env.BRAINCLAW_SESSION_ID?.trim()
|
|
@@ -1649,11 +1816,17 @@ export function parseMcpLine(line) {
|
|
|
1649
1816
|
isNotification: !('id' in parsed),
|
|
1650
1817
|
};
|
|
1651
1818
|
}
|
|
1652
|
-
export function createInitializeResult(protocolVersion) {
|
|
1819
|
+
export function createInitializeResult(protocolVersion, options) {
|
|
1820
|
+
const uninitialized = options?.uninitialized === true;
|
|
1653
1821
|
return {
|
|
1654
1822
|
protocolVersion,
|
|
1655
1823
|
serverInfo: { name: 'brainclaw', version: SCHEMA_VERSION },
|
|
1656
|
-
|
|
1824
|
+
// listChanged is only advertised in setup mode, where the catalog flips
|
|
1825
|
+
// to the full set once the project memory is initialized.
|
|
1826
|
+
capabilities: { tools: { listChanged: uninitialized } },
|
|
1827
|
+
...(uninitialized
|
|
1828
|
+
? { instructions: buildUninitializedStateMessage(options?.cwd ?? process.cwd()) }
|
|
1829
|
+
: {}),
|
|
1657
1830
|
};
|
|
1658
1831
|
}
|
|
1659
1832
|
export class McpTaskRunner {
|
|
@@ -1776,6 +1949,8 @@ export class McpServerConnection {
|
|
|
1776
1949
|
state = 'pre_init';
|
|
1777
1950
|
protocolVersion;
|
|
1778
1951
|
connectionSessionId;
|
|
1952
|
+
/** True while the project memory at cwd is absent — serves the minimal setup catalog. */
|
|
1953
|
+
uninitializedMode;
|
|
1779
1954
|
/** Version of brainclaw code loaded in this process at boot time. */
|
|
1780
1955
|
bootVersion;
|
|
1781
1956
|
/** Throttle disk version checks — at most once per 60s. */
|
|
@@ -1786,11 +1961,15 @@ export class McpServerConnection {
|
|
|
1786
1961
|
constructor(options) {
|
|
1787
1962
|
this.cwd = options.cwd;
|
|
1788
1963
|
this.send = options.send;
|
|
1964
|
+
this.uninitializedMode = options.uninitialized ?? false;
|
|
1789
1965
|
this.bootVersion = getInstalledBrainclawVersion();
|
|
1790
1966
|
this.taskRunner = new McpTaskRunner({
|
|
1791
1967
|
executeTool: options.executeTool ?? createWorkerToolExecutor(),
|
|
1792
1968
|
onResult: (requestId, outcome) => {
|
|
1793
|
-
|
|
1969
|
+
if (outcome.nextConnectionSessionId !== undefined) {
|
|
1970
|
+
// null = explicit clear (session ended); string = refresh; undefined = no-op
|
|
1971
|
+
this.connectionSessionId = outcome.nextConnectionSessionId ?? undefined;
|
|
1972
|
+
}
|
|
1794
1973
|
// Inject version mismatch advisory if stale
|
|
1795
1974
|
const advisory = this.checkVersionMismatch();
|
|
1796
1975
|
if (advisory && outcome.response.content.length > 0) {
|
|
@@ -1799,17 +1978,43 @@ export class McpServerConnection {
|
|
|
1799
1978
|
...outcome.response.content,
|
|
1800
1979
|
];
|
|
1801
1980
|
}
|
|
1981
|
+
const catalogUnlocked = this.reconcileUninitializedMode();
|
|
1982
|
+
if (catalogUnlocked && outcome.response.content.length > 0) {
|
|
1983
|
+
outcome.response.content = [
|
|
1984
|
+
...outcome.response.content,
|
|
1985
|
+
{ 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.' },
|
|
1986
|
+
];
|
|
1987
|
+
}
|
|
1802
1988
|
// Track usage: append response size to usage.jsonl
|
|
1803
1989
|
if (outcome.toolName) {
|
|
1804
1990
|
this.trackUsage(outcome.toolName, outcome.response);
|
|
1805
1991
|
}
|
|
1806
1992
|
this.sendResult(requestId, outcome.response);
|
|
1993
|
+
if (catalogUnlocked) {
|
|
1994
|
+
this.send({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' });
|
|
1995
|
+
}
|
|
1807
1996
|
},
|
|
1808
1997
|
onInternalError: (requestId, error) => {
|
|
1809
1998
|
this.sendError(requestId, -32603, error instanceof Error ? error.message : 'Internal error');
|
|
1810
1999
|
},
|
|
1811
2000
|
});
|
|
1812
2001
|
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Lazy reconcile: if the server booted in setup mode but the project
|
|
2004
|
+
* memory now exists (initialized via bclaw_setup, bclaw_init_project,
|
|
2005
|
+
* or an out-of-band CLI init), unlock the full catalog.
|
|
2006
|
+
* Returns true exactly once — on the transition.
|
|
2007
|
+
*/
|
|
2008
|
+
reconcileUninitializedMode() {
|
|
2009
|
+
if (!this.uninitializedMode) {
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2012
|
+
if (!memoryExists(this.cwd)) {
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
this.uninitializedMode = false;
|
|
2016
|
+
return true;
|
|
2017
|
+
}
|
|
1813
2018
|
/**
|
|
1814
2019
|
* Compare the version loaded in memory with the version on disk.
|
|
1815
2020
|
* Returns an advisory string if they differ, undefined otherwise.
|
|
@@ -1894,7 +2099,11 @@ export class McpServerConnection {
|
|
|
1894
2099
|
const protocolVersion = resolveRequestedProtocolVersion(params, id ?? null);
|
|
1895
2100
|
this.protocolVersion = protocolVersion;
|
|
1896
2101
|
this.state = 'awaiting_initialized';
|
|
1897
|
-
this.
|
|
2102
|
+
this.reconcileUninitializedMode();
|
|
2103
|
+
this.sendResult(id ?? null, createInitializeResult(protocolVersion, {
|
|
2104
|
+
uninitialized: this.uninitializedMode,
|
|
2105
|
+
cwd: this.cwd,
|
|
2106
|
+
}));
|
|
1898
2107
|
return;
|
|
1899
2108
|
}
|
|
1900
2109
|
if (method === 'notifications/initialized' || method === 'initialized') {
|
|
@@ -1917,6 +2126,15 @@ export class McpServerConnection {
|
|
|
1917
2126
|
}
|
|
1918
2127
|
if (method === 'tools/list') {
|
|
1919
2128
|
if (!isNotification) {
|
|
2129
|
+
this.reconcileUninitializedMode();
|
|
2130
|
+
if (this.uninitializedMode) {
|
|
2131
|
+
this.sendResult(id ?? null, {
|
|
2132
|
+
tools: UNINITIALIZED_PUBLISHED_TOOLS,
|
|
2133
|
+
uninitialized: true,
|
|
2134
|
+
state: buildUninitializedStateMessage(this.cwd),
|
|
2135
|
+
});
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
1920
2138
|
const params = message.params === undefined ? {} : requireObjectParams(message.params, id ?? null);
|
|
1921
2139
|
const catalog = typeof params.catalog === 'string' ? params.catalog : undefined;
|
|
1922
2140
|
const include = typeof params.include === 'string' ? params.include : undefined;
|
|
@@ -1946,6 +2164,19 @@ export class McpServerConnection {
|
|
|
1946
2164
|
return;
|
|
1947
2165
|
}
|
|
1948
2166
|
const args = params.arguments === undefined ? {} : requireObjectParams(params.arguments, id ?? null);
|
|
2167
|
+
this.reconcileUninitializedMode();
|
|
2168
|
+
if (this.uninitializedMode && !UNINITIALIZED_TOOL_NAMES.has(name)) {
|
|
2169
|
+
this.sendResult(id ?? null, toolResponse({
|
|
2170
|
+
content: [{ type: 'text', text: buildUninitializedStateMessage(this.cwd) }],
|
|
2171
|
+
structuredContent: {
|
|
2172
|
+
error: 'uninitialized',
|
|
2173
|
+
cwd: this.cwd,
|
|
2174
|
+
available_tools: [...UNINITIALIZED_TOOL_NAMES],
|
|
2175
|
+
next_action: 'Call bclaw_setup to initialize this repo.',
|
|
2176
|
+
},
|
|
2177
|
+
}, true));
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
1949
2180
|
this.taskRunner.enqueue(id ?? null, {
|
|
1950
2181
|
name,
|
|
1951
2182
|
args,
|
|
@@ -2081,12 +2312,15 @@ export class StdioTransport {
|
|
|
2081
2312
|
}
|
|
2082
2313
|
drainNewline() {
|
|
2083
2314
|
while (true) {
|
|
2084
|
-
|
|
2085
|
-
|
|
2315
|
+
// Search for '\n' (0x0a) at the byte level so a multibyte UTF-8 sequence
|
|
2316
|
+
// that spans two chunks is never split mid-character. Avoids O(n²)
|
|
2317
|
+
// string→buffer reconversion on every iteration.
|
|
2318
|
+
const newlineIndex = this.buffer.indexOf(0x0a);
|
|
2086
2319
|
if (newlineIndex === -1)
|
|
2087
2320
|
return;
|
|
2088
|
-
const
|
|
2089
|
-
this.buffer =
|
|
2321
|
+
const lineBuffer = this.buffer.subarray(0, newlineIndex);
|
|
2322
|
+
this.buffer = this.buffer.subarray(newlineIndex + 1);
|
|
2323
|
+
const line = lineBuffer.toString('utf-8').replace(/\r$/, '');
|
|
2090
2324
|
if (line.trim()) {
|
|
2091
2325
|
this.onMessage(line);
|
|
2092
2326
|
}
|
|
@@ -2095,9 +2329,12 @@ export class StdioTransport {
|
|
|
2095
2329
|
}
|
|
2096
2330
|
export function runMcp() {
|
|
2097
2331
|
const cwd = resolveEffectiveCwd();
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2332
|
+
// No project memory yet: start in setup mode instead of refusing to boot,
|
|
2333
|
+
// so agents can initialize the repo via bclaw_setup without a CLI
|
|
2334
|
+
// shell-out + session reload.
|
|
2335
|
+
const uninitialized = !memoryExists(cwd);
|
|
2336
|
+
if (uninitialized) {
|
|
2337
|
+
console.error(buildUninitializedStateMessage(cwd));
|
|
2101
2338
|
}
|
|
2102
2339
|
const missingWorkerPath = resolveMcpWorkerEntryPath();
|
|
2103
2340
|
if (!fs.existsSync(missingWorkerPath)) {
|
|
@@ -2119,6 +2356,7 @@ export function runMcp() {
|
|
|
2119
2356
|
cwd,
|
|
2120
2357
|
send: adaptiveSend,
|
|
2121
2358
|
executeTool: createWorkerToolExecutor(),
|
|
2359
|
+
uninitialized,
|
|
2122
2360
|
});
|
|
2123
2361
|
transport.onMessage = (line) => connection.handleLine(line);
|
|
2124
2362
|
transport.start();
|
|
@@ -2134,6 +2372,9 @@ function createWorkerToolExecutor() {
|
|
|
2134
2372
|
try {
|
|
2135
2373
|
worker = new Worker(new URL('./mcp-worker.js', import.meta.url), {
|
|
2136
2374
|
workerData: payload,
|
|
2375
|
+
// Capture worker stdout so console.log in tool handlers cannot corrupt
|
|
2376
|
+
// the parent's JSON-RPC stream. Drain captured output to stderr.
|
|
2377
|
+
stdout: true,
|
|
2137
2378
|
});
|
|
2138
2379
|
}
|
|
2139
2380
|
catch (error) {
|
|
@@ -2144,6 +2385,8 @@ function createWorkerToolExecutor() {
|
|
|
2144
2385
|
reject(error);
|
|
2145
2386
|
return;
|
|
2146
2387
|
}
|
|
2388
|
+
// Redirect any worker stdout to the server's stderr (safe for diagnostics)
|
|
2389
|
+
worker.stdout?.pipe(process.stderr);
|
|
2147
2390
|
let settled = false;
|
|
2148
2391
|
const cleanup = () => {
|
|
2149
2392
|
worker.removeAllListeners('message');
|
|
@@ -2294,7 +2537,10 @@ function getCrossProjectArg(args, ...keys) {
|
|
|
2294
2537
|
return undefined;
|
|
2295
2538
|
}
|
|
2296
2539
|
function blockCrossProjectExecution(entity, args) {
|
|
2297
|
-
|
|
2540
|
+
// Include 'project' so that canonical write verbs that accept a project routing
|
|
2541
|
+
// arg (e.g. bclaw_claim project=...) are subject to the same signaling-only
|
|
2542
|
+
// boundary as the explicit cross-project keys.
|
|
2543
|
+
const targetProject = getCrossProjectArg(args, 'targetProject', 'target_project', 'crossProject', 'cross_project', 'project');
|
|
2298
2544
|
if (!targetProject) {
|
|
2299
2545
|
return undefined;
|
|
2300
2546
|
}
|
|
@@ -2384,20 +2630,24 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2384
2630
|
if (detected) {
|
|
2385
2631
|
summary.push(`✔ Agent detected: ${detected.name}`);
|
|
2386
2632
|
}
|
|
2387
|
-
summary.push('✔
|
|
2388
|
-
//
|
|
2633
|
+
summary.push('✔ Full brainclaw MCP catalog activates automatically; reload your agent session only if new tools do not appear.');
|
|
2634
|
+
// Bootstrap route follows the shared empty-memory rule; the preview
|
|
2635
|
+
// already embeds the same recommendation text when memory is empty.
|
|
2389
2636
|
const probe = probeForQuickSetup(cwd);
|
|
2390
2637
|
const bootstrapAvailable = probe.hasContent;
|
|
2638
|
+
const emptyMemoryRec = resolveEmptyMemoryRecommendation(cwd);
|
|
2391
2639
|
const preview = buildOnboardingPreview(cwd);
|
|
2392
2640
|
return {
|
|
2393
2641
|
response: toolResponse({
|
|
2394
|
-
content: [{ type: 'text', text: summary.join('\n') +
|
|
2642
|
+
content: [{ type: 'text', text: summary.join('\n') + '\n\n' + preview }],
|
|
2395
2643
|
structuredContent: {
|
|
2396
2644
|
setup_complete: true,
|
|
2397
2645
|
project_type: projectType,
|
|
2398
2646
|
topology,
|
|
2399
2647
|
detected_agent: detected?.name ?? null,
|
|
2400
2648
|
bootstrap_available: bootstrapAvailable,
|
|
2649
|
+
bootstrap_route: emptyMemoryRec.route,
|
|
2650
|
+
next_action: emptyMemoryRec.mcp_next_action,
|
|
2401
2651
|
preview,
|
|
2402
2652
|
summary,
|
|
2403
2653
|
},
|
|
@@ -2578,7 +2828,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2578
2828
|
});
|
|
2579
2829
|
const signal = writeCrossProjectSignal(resolveCrossProjectWritableTarget(crossProjectTarget, 'runtime_note', cwd), 'runtime_note', {
|
|
2580
2830
|
schema_version: 2,
|
|
2581
|
-
id: generateId('
|
|
2831
|
+
id: generateId('runtime_note'),
|
|
2582
2832
|
agent: opIdentity.agent,
|
|
2583
2833
|
agent_id: opIdentity.agent_id,
|
|
2584
2834
|
project_id: opIdentity.project_id ?? '',
|
|
@@ -2648,7 +2898,20 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2648
2898
|
}
|
|
2649
2899
|
}
|
|
2650
2900
|
const identity = resolved.identity;
|
|
2651
|
-
|
|
2901
|
+
// Caller-asserted classification wins (pln#542): the calling agent
|
|
2902
|
+
// declares decision/trap/constraint/note; keyword heuristics are the
|
|
2903
|
+
// fallback when no type is given.
|
|
2904
|
+
const assertedType = typeof args.type === 'string' ? args.type.trim().toLowerCase() : undefined;
|
|
2905
|
+
let classification;
|
|
2906
|
+
if (assertedType === 'decision' || assertedType === 'trap' || assertedType === 'constraint' || assertedType === 'note') {
|
|
2907
|
+
classification = { target: assertedType, reason: 'caller_asserted', decisionScore: 0, trapScore: 0 };
|
|
2908
|
+
}
|
|
2909
|
+
else if (assertedType !== undefined) {
|
|
2910
|
+
return { response: createToolErrorResponse('validation_error', `type must be one of: decision, trap, constraint, note (got '${assertedType}')`) };
|
|
2911
|
+
}
|
|
2912
|
+
else {
|
|
2913
|
+
classification = classifyQuickCapture(text);
|
|
2914
|
+
}
|
|
2652
2915
|
if (classification.target === 'note') {
|
|
2653
2916
|
const result = createRuntimeNote(formatQuickCaptureNoteText(text, context), {
|
|
2654
2917
|
agent: identity.agent_name,
|
|
@@ -2670,6 +2933,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2670
2933
|
note_id: result.noteId,
|
|
2671
2934
|
session_id: result.sessionId,
|
|
2672
2935
|
context,
|
|
2936
|
+
next_actions: [
|
|
2937
|
+
{ 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' },
|
|
2938
|
+
],
|
|
2673
2939
|
},
|
|
2674
2940
|
}),
|
|
2675
2941
|
nextConnectionSessionId: explicitSessionIdFromEnv() ? undefined : result.sessionId,
|
|
@@ -2687,6 +2953,13 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2687
2953
|
const statusText = capture.writeThrough
|
|
2688
2954
|
? `✔ Quick capture promoted as ${classification.target} [${capture.promotedItemId}]`
|
|
2689
2955
|
: `✔ Quick capture saved as ${classification.target} candidate [${capture.candidateId}]`;
|
|
2956
|
+
const captureNextActions = capture.writeThrough
|
|
2957
|
+
? [
|
|
2958
|
+
{ tool: 'bclaw_get', args: { entity: classification.target, id: capture.promotedItemId }, when: 'to verify the promoted item' },
|
|
2959
|
+
]
|
|
2960
|
+
: [
|
|
2961
|
+
{ tool: 'bclaw_get', args: { entity: 'candidate', id: capture.candidateId }, when: 'to review the pending candidate (contradiction metadata is advisory)' },
|
|
2962
|
+
];
|
|
2690
2963
|
return {
|
|
2691
2964
|
response: toolResponse({
|
|
2692
2965
|
content: [{ type: 'text', text: statusText }],
|
|
@@ -2698,7 +2971,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2698
2971
|
candidate_id: capture.candidateId,
|
|
2699
2972
|
promoted_item_id: capture.promotedItemId,
|
|
2700
2973
|
write_through: capture.writeThrough,
|
|
2701
|
-
|
|
2974
|
+
// Advisory only (cnd_abe61d68): contradictions are metadata on
|
|
2975
|
+
// the candidate, never a promotion blocker.
|
|
2702
2976
|
contradiction_summary: capture.contradictionSummary,
|
|
2703
2977
|
contradictions_detected: capture.contradictionsDetected?.map((item) => ({
|
|
2704
2978
|
severity: item.severity,
|
|
@@ -2706,9 +2980,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2706
2980
|
conflicts_with: item.conflicts_with,
|
|
2707
2981
|
})),
|
|
2708
2982
|
context,
|
|
2983
|
+
next_actions: captureNextActions,
|
|
2709
2984
|
},
|
|
2710
2985
|
}),
|
|
2711
|
-
nextConnectionSessionId: undefined,
|
|
2712
2986
|
};
|
|
2713
2987
|
}
|
|
2714
2988
|
if (name === 'bclaw_create_plan') {
|
|
@@ -3037,10 +3311,29 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3037
3311
|
catch {
|
|
3038
3312
|
return { response: createToolErrorResponse('not_found', `Claim not found: ${claimId}`) };
|
|
3039
3313
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
});
|
|
3314
|
+
// pln#562 step 5 — release is ownership-checked like acquisition and
|
|
3315
|
+
// adoption. The resolved principal must own the claim; trusted+ callers
|
|
3316
|
+
// (coordinators) may release others' claims, with an audit entry.
|
|
3317
|
+
const releaseIdentity = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
3318
|
+
const releaseAuth = 'identity' in releaseIdentity && releaseIdentity.identity
|
|
3319
|
+
? {
|
|
3320
|
+
agent: releaseIdentity.identity.agent_name,
|
|
3321
|
+
agent_id: releaseIdentity.identity.agent_id,
|
|
3322
|
+
session_id: connectionSessionId,
|
|
3323
|
+
override: hasMinimumTrustLevel(releaseIdentity.identity.trust_level ?? 'contributor', 'trusted'),
|
|
3324
|
+
}
|
|
3325
|
+
: undefined;
|
|
3326
|
+
let cascadeResult;
|
|
3327
|
+
try {
|
|
3328
|
+
cascadeResult = releaseClaimWithCascade(claimId, {
|
|
3329
|
+
planStatus: args.planStatus,
|
|
3330
|
+
cwd,
|
|
3331
|
+
auth: releaseAuth,
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
catch (err) {
|
|
3335
|
+
return { response: createToolErrorResponse('trust_error', err instanceof Error ? err.message : String(err)) };
|
|
3336
|
+
}
|
|
3044
3337
|
const { planTransitioned, planWarning, planId: cascadePlanId, newPlanStatus: cascadeNewStatus } = cascadeResult;
|
|
3045
3338
|
const summaryText = [
|
|
3046
3339
|
`✔ Released claim [${claimId}]`,
|
|
@@ -3255,7 +3548,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3255
3548
|
...(result.handoff ? { handoff: result.handoff } : {}),
|
|
3256
3549
|
...(result.reflection_prompt ? { reflection_prompt: result.reflection_prompt } : {}),
|
|
3257
3550
|
}),
|
|
3258
|
-
nextConnectionSessionId:
|
|
3551
|
+
nextConnectionSessionId: null,
|
|
3259
3552
|
};
|
|
3260
3553
|
}
|
|
3261
3554
|
if (name === 'bclaw_compact') {
|
|
@@ -3389,20 +3682,41 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3389
3682
|
}
|
|
3390
3683
|
lines.push(` Sequence: ${analysis.sequence.name}`);
|
|
3391
3684
|
lines.push(` Ready: ${analysis.ready.length} | Active: ${analysis.active.length} | Blocked: ${analysis.blocked.length} | Done: ${analysis.done.length}`);
|
|
3685
|
+
// can_681a6c52 — truthful per-delivery status. The old output printed
|
|
3686
|
+
// '[inbox]' for every entry and ALWAYS dumped a 'Run these commands'
|
|
3687
|
+
// block, even when the auto-spawn had already SUCCEEDED — an obedient
|
|
3688
|
+
// coordinator would then double-spawn the worker.
|
|
3689
|
+
const spawnedPlanIds = new Set(dispatchResult.messages_sent
|
|
3690
|
+
.filter((m) => m.execution_status === 'delivered_and_started')
|
|
3691
|
+
.map((m) => m.plan_id));
|
|
3392
3692
|
if (dispatchResult.messages_sent.length > 0) {
|
|
3393
3693
|
lines.push('');
|
|
3394
3694
|
lines.push(args.dryRun ? ' Would assign:' : ' Assigned:');
|
|
3395
3695
|
for (const msg of dispatchResult.messages_sent) {
|
|
3396
3696
|
const lane = msg.lane ? ` (lane: ${msg.lane})` : '';
|
|
3397
|
-
|
|
3697
|
+
const exec = msg.execution_status ? ` [${msg.execution_status}]` : ' [inbox]';
|
|
3698
|
+
const pid = msg.pid ? ` pid=${msg.pid}` : '';
|
|
3699
|
+
const run = msg.run_id ? ` run=${msg.run_id}` : '';
|
|
3700
|
+
lines.push(` ${msg.agent}: ${msg.plan_id}${lane}${exec}${pid}${run}`);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
const spawned = dispatchResult.messages_sent.filter((m) => m.execution_status === 'delivered_and_started');
|
|
3704
|
+
if (spawned.length > 0) {
|
|
3705
|
+
lines.push('');
|
|
3706
|
+
lines.push('Auto-spawn succeeded — do NOT run the launch commands for these (double-spawn risk). Verify instead:');
|
|
3707
|
+
for (const msg of spawned) {
|
|
3708
|
+
const target = msg.assignment_id ?? msg.run_id ?? msg.claim_id;
|
|
3709
|
+
lines.push(` bclaw_dispatch_status(target_id: "${target}") # ${msg.agent} on ${msg.plan_id}`);
|
|
3398
3710
|
}
|
|
3399
3711
|
}
|
|
3400
|
-
//
|
|
3401
|
-
|
|
3712
|
+
// Only surface manual launch commands for deliveries that were NOT
|
|
3713
|
+
// auto-spawned (manual fallback, spawn refusal, inbox-only).
|
|
3714
|
+
const manualCommands = dispatchResult.commands.filter((cmd) => !cmd.plan_id || !spawnedPlanIds.has(cmd.plan_id));
|
|
3715
|
+
if (manualCommands.length > 0) {
|
|
3402
3716
|
lines.push('');
|
|
3403
|
-
lines.push('Run these commands to launch the
|
|
3717
|
+
lines.push('Run these commands to launch the agents that were NOT auto-spawned:');
|
|
3404
3718
|
lines.push('');
|
|
3405
|
-
for (const cmd of
|
|
3719
|
+
for (const cmd of manualCommands) {
|
|
3406
3720
|
const lane = cmd.lane ? ` [lane: ${cmd.lane}]` : '';
|
|
3407
3721
|
lines.push(`# ${cmd.agent}${lane}`);
|
|
3408
3722
|
lines.push(cmd.command);
|
|
@@ -3416,6 +3730,15 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3416
3730
|
lines.push(` - ${skip.plan_id}: ${skip.reason}`);
|
|
3417
3731
|
}
|
|
3418
3732
|
}
|
|
3733
|
+
// can_45316d5c — worktree warnings / spawn refusals were hidden behind
|
|
3734
|
+
// dispatch_status; surface them in the cycle output itself.
|
|
3735
|
+
if (dispatchResult.warnings.length > 0) {
|
|
3736
|
+
lines.push('');
|
|
3737
|
+
lines.push(' Warnings:');
|
|
3738
|
+
for (const warning of dispatchResult.warnings) {
|
|
3739
|
+
lines.push(` ⚠ ${warning}`);
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3419
3742
|
appendAuditEntry({
|
|
3420
3743
|
actor: resolved.identity.agent_name,
|
|
3421
3744
|
actor_id: resolved.identity.agent_id,
|
|
@@ -3498,7 +3821,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3498
3821
|
if (status === 'accepted' && assignment.message_id) {
|
|
3499
3822
|
try {
|
|
3500
3823
|
const { ackMessage } = await import('../core/messaging.js');
|
|
3501
|
-
|
|
3824
|
+
// pln#562 step 4 — scope the ack to this assignment's claim so a
|
|
3825
|
+
// same-named sibling instance cannot consume the message.
|
|
3826
|
+
ackMessage(assignment.message_id, callerAgent, cwd, { claimId: assignment.claim_id });
|
|
3502
3827
|
}
|
|
3503
3828
|
catch { /* best-effort: don't fail the update if ack fails */ }
|
|
3504
3829
|
}
|
|
@@ -3665,7 +3990,11 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3665
3990
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: id') };
|
|
3666
3991
|
}
|
|
3667
3992
|
try {
|
|
3668
|
-
|
|
3993
|
+
// pln#562 step 4 — a dispatched instance (BRAINCLAW_CLAIM_ID) may only
|
|
3994
|
+
// ack messages bound to its own claim.
|
|
3995
|
+
const result = ackMessage(msgId, resolved.identity.agent_name, cwd, {
|
|
3996
|
+
claimId: process.env.BRAINCLAW_CLAIM_ID?.trim() || undefined,
|
|
3997
|
+
});
|
|
3669
3998
|
appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'update', item_id: result.id, item_type: 'message' }, cwd);
|
|
3670
3999
|
return {
|
|
3671
4000
|
response: toolResponse({
|
|
@@ -3767,13 +4096,15 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3767
4096
|
const stepTextRaw = stepData.text ?? stepData.title ?? args.text;
|
|
3768
4097
|
const stepText = typeof stepTextRaw === 'string' ? stepTextRaw.trim() : '';
|
|
3769
4098
|
const stepAssignee = (stepData.assignee ?? args.assignee);
|
|
4099
|
+
const stepEstimated = (stepData.estimated_effort ?? args.estimated_effort);
|
|
4100
|
+
const stepActual = (stepData.actual_effort ?? args.actual_effort);
|
|
3770
4101
|
if (!stepPlanId)
|
|
3771
4102
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
3772
4103
|
if (!stepText)
|
|
3773
4104
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: data.text') };
|
|
3774
4105
|
const stepTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3775
4106
|
try {
|
|
3776
|
-
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee: stepAssignee }, stepTargetCwd);
|
|
4107
|
+
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee: stepAssignee, estimatedEffort: stepEstimated, actualEffort: stepActual }, stepTargetCwd);
|
|
3777
4108
|
return {
|
|
3778
4109
|
response: toolResponse({
|
|
3779
4110
|
content: [{ type: 'text', text: `✔ Step added: [${result.stepId}] ${stepText} (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
@@ -3854,6 +4185,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3854
4185
|
status: args.status,
|
|
3855
4186
|
text: args.text,
|
|
3856
4187
|
assignee: args.assignee,
|
|
4188
|
+
estimatedEffort: args.estimated_effort,
|
|
4189
|
+
actualEffort: args.actual_effort,
|
|
3857
4190
|
}, usTargetCwd);
|
|
3858
4191
|
const changes = [];
|
|
3859
4192
|
if (args.status)
|
|
@@ -3862,6 +4195,10 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3862
4195
|
changes.push('text updated');
|
|
3863
4196
|
if (args.assignee !== undefined)
|
|
3864
4197
|
changes.push(`assignee=${args.assignee || 'unassigned'}`);
|
|
4198
|
+
if (args.estimated_effort !== undefined)
|
|
4199
|
+
changes.push(`estimate=${args.estimated_effort}`);
|
|
4200
|
+
if (args.actual_effort !== undefined)
|
|
4201
|
+
changes.push(`actual=${args.actual_effort}`);
|
|
3865
4202
|
return {
|
|
3866
4203
|
response: toolResponse({
|
|
3867
4204
|
content: [{ type: 'text', text: `✔ Step updated: [${result.stepId}] ${changes.join(', ')} (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
@@ -4289,11 +4626,11 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4289
4626
|
if (sessionResult.auto_registered) {
|
|
4290
4627
|
warnings.push(`Agent '${sessionResult.agent}' was auto-registered (first use). Run \`brainclaw register-agent ${sessionResult.agent}\` to set capabilities and trust level.`);
|
|
4291
4628
|
}
|
|
4292
|
-
// Step 2: build context for requested scope.
|
|
4293
|
-
//
|
|
4294
|
-
//
|
|
4295
|
-
//
|
|
4296
|
-
//
|
|
4629
|
+
// Step 2: build context for requested scope. The "what's new" diff is
|
|
4630
|
+
// surfaced for ALL intents (pln#390 regression fix): intent='resume'
|
|
4631
|
+
// anchors it on the agent's previous session; every other intent gets
|
|
4632
|
+
// the per-agent event-log-cursor diff computed inside buildContext
|
|
4633
|
+
// (pln#542 — converged novelty mechanism, covers status transitions).
|
|
4297
4634
|
let contextResult;
|
|
4298
4635
|
try {
|
|
4299
4636
|
let sinceSession;
|
|
@@ -4307,6 +4644,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4307
4644
|
agent: sessionResult.agent,
|
|
4308
4645
|
cwd: targetCwd,
|
|
4309
4646
|
sinceSession,
|
|
4647
|
+
// ~4 chars/token: relevance-ranked fill up to the caller's budget.
|
|
4648
|
+
maxChars: workReq.budget_tokens ? workReq.budget_tokens * 4 : undefined,
|
|
4310
4649
|
});
|
|
4311
4650
|
}
|
|
4312
4651
|
catch { /* non-fatal — context failure should not block work */ }
|
|
@@ -4367,11 +4706,30 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4367
4706
|
text: w.text.slice(0, 80),
|
|
4368
4707
|
age_days: w.age_days,
|
|
4369
4708
|
}));
|
|
4709
|
+
// pln#390 regression fix: the compact projection used to silently
|
|
4710
|
+
// drop context_diff, defeating the diff-first contract. Keep a
|
|
4711
|
+
// trimmed view (summary + counts + top-5 items, text capped).
|
|
4712
|
+
const trimmedDiff = contextResult.context_diff
|
|
4713
|
+
? {
|
|
4714
|
+
since: contextResult.context_diff.since,
|
|
4715
|
+
since_session: contextResult.context_diff.since_session,
|
|
4716
|
+
source: contextResult.context_diff.source,
|
|
4717
|
+
summary: contextResult.context_diff.summary,
|
|
4718
|
+
counts: contextResult.context_diff.counts,
|
|
4719
|
+
changed_items: (contextResult.context_diff.changed_items ?? [])
|
|
4720
|
+
.slice(0, 5)
|
|
4721
|
+
.map((item) => ({ ...item, text: item.text.slice(0, 120) })),
|
|
4722
|
+
...(contextResult.context_diff.unseen_event_count !== undefined
|
|
4723
|
+
? { unseen_event_count: contextResult.context_diff.unseen_event_count }
|
|
4724
|
+
: {}),
|
|
4725
|
+
}
|
|
4726
|
+
: undefined;
|
|
4370
4727
|
resultPayload = {
|
|
4371
4728
|
context_schema: contextResult.context_schema,
|
|
4372
4729
|
profile: contextResult.profile,
|
|
4373
4730
|
memory_version: contextResult.memory_version,
|
|
4374
4731
|
memory_density: contextResult.memory_density,
|
|
4732
|
+
context_diff: trimmedDiff ?? null,
|
|
4375
4733
|
plan_summary: planItems,
|
|
4376
4734
|
stale_warnings: staleTop3,
|
|
4377
4735
|
workflow_hints: (contextResult.workflow_hints ?? []).slice(0, 3),
|
|
@@ -4381,34 +4739,88 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4381
4739
|
_full_context_hint: 'Use bclaw_context(kind="memory") for the full payload.',
|
|
4382
4740
|
};
|
|
4383
4741
|
}
|
|
4384
|
-
// pln#513 step 1 — bootstrap hint.
|
|
4385
|
-
// PROJECT.md (
|
|
4386
|
-
//
|
|
4387
|
-
//
|
|
4388
|
-
//
|
|
4742
|
+
// pln#513 step 1 / pln#557 step 3 — bootstrap hint. The original probe
|
|
4743
|
+
// was a one-bit PROJECT.md stat(): false positive on a rich store
|
|
4744
|
+
// without PROJECT.md (recommended from-scratch bootstrap over 17k
|
|
4745
|
+
// events), eternal false negative on a fossil PROJECT.md. The composite
|
|
4746
|
+
// assessment (assessBootstrapNeed: presence × mtime-vs-activity ×
|
|
4747
|
+
// store density) adds a distinct 'refresh' verdict that maps to
|
|
4748
|
+
// bclaw_bootstrap(refresh: true) — coordinate with the pln#514 step 1
|
|
4749
|
+
// force-flag. 'bootstrap' keeps the shared empty-memory rule
|
|
4750
|
+
// (resolveEmptyMemoryRecommendation): repo with content → extract via
|
|
4751
|
+
// bclaw_bootstrap; greenfield → bootstrap loop. Both chainable.
|
|
4389
4752
|
let bootstrapRecommended;
|
|
4753
|
+
let bootstrapVerdict;
|
|
4754
|
+
let bootstrapRefreshReason;
|
|
4390
4755
|
let nextAction;
|
|
4756
|
+
let emptyMemoryRec;
|
|
4757
|
+
let storeDensity;
|
|
4391
4758
|
try {
|
|
4392
|
-
const
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
needsBootstrap = true;
|
|
4400
|
-
}
|
|
4401
|
-
catch {
|
|
4402
|
-
needsBootstrap = true; // ENOENT → absent
|
|
4759
|
+
const assessment = assessBootstrapNeed(targetCwd);
|
|
4760
|
+
bootstrapVerdict = assessment.verdict;
|
|
4761
|
+
bootstrapRecommended = assessment.verdict !== 'none';
|
|
4762
|
+
storeDensity = assessment.store_density;
|
|
4763
|
+
if (assessment.verdict === 'bootstrap') {
|
|
4764
|
+
emptyMemoryRec = resolveEmptyMemoryRecommendation(targetCwd);
|
|
4765
|
+
nextAction = emptyMemoryRec.mcp_next_action;
|
|
4403
4766
|
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
nextAction =
|
|
4767
|
+
else if (assessment.verdict === 'refresh') {
|
|
4768
|
+
bootstrapRefreshReason = assessment.reasons[0];
|
|
4769
|
+
nextAction = 'bclaw_bootstrap(refresh: true)';
|
|
4407
4770
|
}
|
|
4408
4771
|
}
|
|
4409
4772
|
catch {
|
|
4410
4773
|
// Best-effort: never block bclaw_work on the probe.
|
|
4411
4774
|
}
|
|
4775
|
+
// Self-teaching affordances (pln#542): each response carries the
|
|
4776
|
+
// recommended follow-up calls with exact shapes.
|
|
4777
|
+
const nextActions = [];
|
|
4778
|
+
if (bootstrapVerdict === 'bootstrap' && emptyMemoryRec) {
|
|
4779
|
+
if (emptyMemoryRec.route === 'extract') {
|
|
4780
|
+
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` });
|
|
4781
|
+
}
|
|
4782
|
+
else {
|
|
4783
|
+
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` });
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
else if (bootstrapVerdict === 'refresh') {
|
|
4787
|
+
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' });
|
|
4788
|
+
}
|
|
4789
|
+
if (claimId) {
|
|
4790
|
+
nextActions.push({ tool: 'bclaw_release_claim', args: { id: claimId, planStatus: 'done' }, when: 'implementation complete and committed' });
|
|
4791
|
+
}
|
|
4792
|
+
else if (workReq.intent === 'consult' || workReq.intent === 'resume') {
|
|
4793
|
+
nextActions.push({ tool: 'bclaw_work', args: { intent: 'execute', scope: workReq.scope ?? '<scope>' }, when: 'ready to edit — claims the scope' });
|
|
4794
|
+
}
|
|
4795
|
+
// Solo-agent empty-store hint: the bootstrap recommendation covers
|
|
4796
|
+
// vision; agents arriving on a freshly-initialised store also need a
|
|
4797
|
+
// surface for *work* itself. Without this they reliably consult, see
|
|
4798
|
+
// nothing, and stop — bclaw_create(entity='plan') is the missing
|
|
4799
|
+
// affordance (2026-06-10 front-door audit). The store-density signal
|
|
4800
|
+
// bumps to 'low' as soon as session_start lands a single event, so
|
|
4801
|
+
// gate the hint directly on "no plans yet" — the actual condition
|
|
4802
|
+
// the agent is in.
|
|
4803
|
+
let noPlansYet = false;
|
|
4804
|
+
try {
|
|
4805
|
+
noPlansYet = loadState(targetCwd).plan_items.length === 0;
|
|
4806
|
+
}
|
|
4807
|
+
catch {
|
|
4808
|
+
// loadState may fail on a brand-new store with no project.md yet;
|
|
4809
|
+
// treat that as "no plans yet" — the next_action remains correct.
|
|
4810
|
+
noPlansYet = true;
|
|
4811
|
+
}
|
|
4812
|
+
if (noPlansYet && (storeDensity === 'empty' || storeDensity === 'low')) {
|
|
4813
|
+
nextActions.push({
|
|
4814
|
+
tool: 'bclaw_create',
|
|
4815
|
+
args: { entity: 'plan', input: { title: '<plan title>', steps: ['<first step>'] } },
|
|
4816
|
+
when: 'memory has no plans yet — once you know what you are doing, create a plan so progress is tracked',
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4819
|
+
const diffTotal = contextResult?.context_diff?.counts.total ?? 0;
|
|
4820
|
+
if (useCompact && diffTotal > 0) {
|
|
4821
|
+
nextActions.push({ tool: 'bclaw_context', args: { kind: 'memory' }, when: 'to read the full changed items behind context_diff' });
|
|
4822
|
+
}
|
|
4823
|
+
nextActions.push({ tool: 'bclaw_quick_capture', args: { text: '<finding>', type: '<decision|trap|constraint|note>' }, when: 'capture decisions/traps as you work' });
|
|
4412
4824
|
const facadeResponse = {
|
|
4413
4825
|
status: 'ok',
|
|
4414
4826
|
intent: workReq.intent,
|
|
@@ -4420,15 +4832,23 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4420
4832
|
warnings,
|
|
4421
4833
|
duration_ms: Date.now() - startMs,
|
|
4422
4834
|
bootstrap_recommended: bootstrapRecommended,
|
|
4835
|
+
bootstrap_verdict: bootstrapVerdict,
|
|
4423
4836
|
next_action: nextAction,
|
|
4837
|
+
next_actions: nextActions,
|
|
4424
4838
|
};
|
|
4425
4839
|
const summaryParts = [`✔ bclaw_work [${workReq.intent}] session=${sessionResult.session_id}`];
|
|
4426
4840
|
if (claimId)
|
|
4427
4841
|
summaryParts.push(`claim=${claimId} (${claimStatus})`);
|
|
4842
|
+
if (contextResult?.context_diff && diffTotal > 0) {
|
|
4843
|
+
summaryParts.push(`Δ since last look: ${contextResult.context_diff.summary}`);
|
|
4844
|
+
}
|
|
4428
4845
|
if (useCompact)
|
|
4429
4846
|
summaryParts.push('mode=compact (use bclaw_context for full payload)');
|
|
4430
|
-
if (
|
|
4431
|
-
summaryParts.push(`💡
|
|
4847
|
+
if (bootstrapVerdict === 'bootstrap' && emptyMemoryRec) {
|
|
4848
|
+
summaryParts.push(`💡 ${emptyMemoryRec.text}`);
|
|
4849
|
+
}
|
|
4850
|
+
else if (bootstrapVerdict === 'refresh') {
|
|
4851
|
+
summaryParts.push(`💡 ${bootstrapRefreshReason ?? 'PROJECT.md is missing or fossil relative to a mature store'} → bclaw_bootstrap(refresh: true)`);
|
|
4432
4852
|
}
|
|
4433
4853
|
if (warnings.length > 0)
|
|
4434
4854
|
summaryParts.push(warnings.map((w) => `⚠ ${w}`).join('\n'));
|
|
@@ -4576,6 +4996,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4576
4996
|
dispatcherAgent: opts.senderAgent,
|
|
4577
4997
|
dispatcherAgentId: opts.senderAgentId,
|
|
4578
4998
|
cwd: opts.cwd,
|
|
4999
|
+
requireWorktree: true, // pln#531: never spawn a worker in the integration repo
|
|
4579
5000
|
});
|
|
4580
5001
|
entry.execution_status = execResult.execution_status;
|
|
4581
5002
|
if (execResult.pid)
|
|
@@ -4952,6 +5373,31 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4952
5373
|
loopReviewerAgents = resolvedAgents.slice(0, REVIEW_OPEN_LOOP_FANOUT_CAP);
|
|
4953
5374
|
preReviewWarnings.push(`open_loop: implicit reviewer fan-out capped at ${REVIEW_OPEN_LOOP_FANOUT_CAP} of ${resolvedAgents.length} spawnable agents; pass targetAgents to override`);
|
|
4954
5375
|
}
|
|
5376
|
+
// pln#533 — pre-flight the reviewer agents with a trivial validation
|
|
5377
|
+
// spawn BEFORE opening the loop, so an environment death (config
|
|
5378
|
+
// rejected, auth fail, model mismatch) surfaces instantly with a clear
|
|
5379
|
+
// reason instead of a generic "did not acknowledge" loop timeout. Drop
|
|
5380
|
+
// the agents that fail and surface their reasons; if none survive the
|
|
5381
|
+
// existing length===0 guard skips loop creation. Skipped when open_loop
|
|
5382
|
+
// is off, preflight=false, or BRAINCLAW_NO_SPAWN is set (handled inside
|
|
5383
|
+
// preflightAgents). Cross-project dispatch never auto-spawns, so skip.
|
|
5384
|
+
if (req.open_loop === true && req.preflight !== false && !req.project && loopReviewerAgents.length > 0) {
|
|
5385
|
+
try {
|
|
5386
|
+
const { preflightAgents } = await import('../core/spawn-check.js');
|
|
5387
|
+
const pf = await preflightAgents(loopReviewerAgents, { cwd: dispatchCwd });
|
|
5388
|
+
if (pf.blocked.length > 0) {
|
|
5389
|
+
const healthy = loopReviewerAgents.filter((a) => !pf.blocked.some((b) => b.agent === a));
|
|
5390
|
+
for (const b of pf.blocked) {
|
|
5391
|
+
preReviewWarnings.push(`pre-flight: dropped reviewer '${b.agent}' — ${b.reason}.${b.recommended_next_action ? ` ${b.recommended_next_action}` : ''}`);
|
|
5392
|
+
}
|
|
5393
|
+
loopReviewerAgents = healthy;
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
catch (pfErr) {
|
|
5397
|
+
// Pre-flight is best-effort: a failure here must not block the review.
|
|
5398
|
+
preReviewWarnings.push(`pre-flight: skipped (check threw: ${pfErr instanceof Error ? pfErr.message : String(pfErr)})`);
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
4955
5401
|
// Lazy-import the loops module once before defining performReview so
|
|
4956
5402
|
// the synchronous withLoopLock work callback can use it without
|
|
4957
5403
|
// re-importing inside the callback.
|
|
@@ -5705,6 +6151,23 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5705
6151
|
};
|
|
5706
6152
|
}
|
|
5707
6153
|
if (name === 'bclaw_loop') {
|
|
6154
|
+
// pln#542: intent='open' is no longer exposed standalone over MCP — it
|
|
6155
|
+
// creates a loop without dispatching the first turn (the documented
|
|
6156
|
+
// anti-pattern, now removed instead of documented). Internal callers
|
|
6157
|
+
// (bclaw_coordinate, CLI bootstrap) use core openLoop directly.
|
|
6158
|
+
if (args?.intent === 'open') {
|
|
6159
|
+
return {
|
|
6160
|
+
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."),
|
|
6161
|
+
};
|
|
6162
|
+
}
|
|
6163
|
+
// pln#562 step 4 — dispatching a turn hands work to another agent; gate
|
|
6164
|
+
// it at the same trust bar as the other dispatch surfaces.
|
|
6165
|
+
if (args?.intent === 'turn' && args?.dispatch === true) {
|
|
6166
|
+
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
6167
|
+
if (resolved.error) {
|
|
6168
|
+
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
5708
6171
|
const { handleBclawLoop } = await import('./loops-handlers.js');
|
|
5709
6172
|
const targetCwd = resolveProjectCwd(args?.project, cwd);
|
|
5710
6173
|
const result = handleBclawLoop({ args: args, cwd: targetCwd });
|
|
@@ -5798,9 +6261,19 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5798
6261
|
// this caps SIZE) so a verbose result set never overflows the MCP token
|
|
5799
6262
|
// cap and silently pushes the agent to the CLI (trp#449). Advertises
|
|
5800
6263
|
// has_more / next_offset / hint for explicit pagination.
|
|
6264
|
+
// pln#542: budget_tokens lets the caller shrink the size cap further
|
|
6265
|
+
// (~4 chars/token); it can only tighten, never exceed the default.
|
|
5801
6266
|
const offset = Math.max(0, Number(filter.offset) || 0);
|
|
5802
|
-
const
|
|
6267
|
+
const budgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
6268
|
+
const charBudget = budgetTokens ? Math.min(budgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
|
|
6269
|
+
const bounded = boundListResult(result, offset, charBudget);
|
|
5803
6270
|
const warnings = collectLoadValidationWarnings(entity, targetCwd);
|
|
6271
|
+
const nextActions = [
|
|
6272
|
+
{ tool: 'bclaw_get', args: { entity, id: '<id from items>' }, when: 'to read one item in full' },
|
|
6273
|
+
];
|
|
6274
|
+
if (bounded.has_more) {
|
|
6275
|
+
nextActions.push({ tool: 'bclaw_find', args: { entity, filter: { ...filter, offset: bounded.next_offset } }, when: 'to fetch the next page' });
|
|
6276
|
+
}
|
|
5804
6277
|
// structuredContent is the canonical MCP return channel that clients
|
|
5805
6278
|
// (VS Code extension, Codex, etc.) read for machine-parseable data.
|
|
5806
6279
|
// Prior to this fix we spread `...result` at top-level of the
|
|
@@ -5811,7 +6284,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5811
6284
|
return {
|
|
5812
6285
|
response: toolResponse({
|
|
5813
6286
|
content: [{ type: 'text', text: `✔ ${result.total} ${entity} item(s)${moreNote}` }],
|
|
5814
|
-
structuredContent: { ...bounded, warnings },
|
|
6287
|
+
structuredContent: { ...bounded, warnings, next_actions: nextActions },
|
|
5815
6288
|
}),
|
|
5816
6289
|
};
|
|
5817
6290
|
}
|
|
@@ -5840,10 +6313,35 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5840
6313
|
};
|
|
5841
6314
|
}
|
|
5842
6315
|
const item = getEntity(entity, id, targetCwd);
|
|
6316
|
+
// trp#449 class (pln#542): handoff snapshots embed an unbounded git
|
|
6317
|
+
// diff — cap it (budget_tokens tightens, ~4 chars/token).
|
|
6318
|
+
let boundedItem = item;
|
|
6319
|
+
let diffTruncated = false;
|
|
6320
|
+
const getBudgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
6321
|
+
const getCharBudget = getBudgetTokens ? Math.min(getBudgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
|
|
6322
|
+
if (entity === 'handoff' && item && typeof item === 'object') {
|
|
6323
|
+
const snapshot = item.snapshot;
|
|
6324
|
+
if (snapshot?.diff && snapshot.diff.length > getCharBudget) {
|
|
6325
|
+
diffTruncated = true;
|
|
6326
|
+
boundedItem = {
|
|
6327
|
+
...item,
|
|
6328
|
+
snapshot: { ...snapshot, diff: `${snapshot.diff.slice(0, getCharBudget)}\n… [diff truncated to ${getCharBudget} chars]` },
|
|
6329
|
+
};
|
|
6330
|
+
}
|
|
6331
|
+
else {
|
|
6332
|
+
const note = handoffDiffPreviewNote(snapshot);
|
|
6333
|
+
if (snapshot?.diff && note) {
|
|
6334
|
+
boundedItem = {
|
|
6335
|
+
...item,
|
|
6336
|
+
snapshot: { ...snapshot, diff: `${snapshot.diff}\n${note}` },
|
|
6337
|
+
};
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
}
|
|
5843
6341
|
return {
|
|
5844
6342
|
response: toolResponse({
|
|
5845
6343
|
content: [{ type: 'text', text: `✔ fetched ${entity} ${id}` }],
|
|
5846
|
-
structuredContent: { entity, item },
|
|
6344
|
+
structuredContent: { entity, item: boundedItem, ...(diffTruncated ? { diff_truncated: true } : {}) },
|
|
5847
6345
|
}),
|
|
5848
6346
|
};
|
|
5849
6347
|
}
|
|
@@ -5854,25 +6352,43 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5854
6352
|
if (name === 'bclaw_create') {
|
|
5855
6353
|
try {
|
|
5856
6354
|
const entity = String(args.entity ?? '');
|
|
6355
|
+
// Execution entities stay local: the signaling-only cross-project
|
|
6356
|
+
// boundary applies to the canonical verbs too, not just legacy tools.
|
|
6357
|
+
if (entity === 'claim' || entity === 'plan') {
|
|
6358
|
+
const crossProjectError = blockCrossProjectExecution(entity, args);
|
|
6359
|
+
if (crossProjectError)
|
|
6360
|
+
return { response: crossProjectError };
|
|
6361
|
+
}
|
|
5857
6362
|
const rawData = (args.data ?? {});
|
|
5858
6363
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
5859
6364
|
// Auto-fill identity fields. Without this, a caller who omits author/agent
|
|
5860
6365
|
// creates a schema-invalid record that is silently dropped on read and
|
|
5861
|
-
// GC'd from disk on the next mutation.
|
|
5862
|
-
// resolved MCP identity → args.agent → 'unknown'.
|
|
6366
|
+
// GC'd from disk on the next mutation.
|
|
5863
6367
|
// Identity is resolved against the SOURCE cwd (the agent's own
|
|
5864
6368
|
// project/registry), not the target — an agent doesn't need to be
|
|
5865
|
-
// registered in the target project to write into it.
|
|
5866
|
-
|
|
6369
|
+
// registered in the target project to write into it. An explicitly
|
|
6370
|
+
// supplied data.author is honored as content-level attribution
|
|
6371
|
+
// (cross-project signaling writers may not be registered locally);
|
|
6372
|
+
// when author is MISSING, resolution is mandatory and failure is a
|
|
6373
|
+
// hard validation_error (pln#562 step 3) — never author:'unknown'.
|
|
5867
6374
|
const data = { ...rawData };
|
|
5868
|
-
|
|
6375
|
+
let actor = typeof data.author === 'string' ? data.author : undefined;
|
|
6376
|
+
let actorId = typeof data.agent_id === 'string' ? data.agent_id : undefined;
|
|
6377
|
+
if (data.author === undefined) {
|
|
6378
|
+
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
5869
6379
|
data.author = agent_name;
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
6380
|
+
if (data.agent === undefined)
|
|
6381
|
+
data.agent = agent_name;
|
|
6382
|
+
if (data.agent_id === undefined && agent_id)
|
|
6383
|
+
data.agent_id = agent_id;
|
|
6384
|
+
actor = agent_name;
|
|
6385
|
+
actorId = agent_id;
|
|
6386
|
+
}
|
|
6387
|
+
else if (data.agent === undefined) {
|
|
6388
|
+
data.agent = data.author;
|
|
6389
|
+
}
|
|
5874
6390
|
const result = createEntity(entity, data, targetCwd);
|
|
5875
|
-
appendAuditEntry({ actor:
|
|
6391
|
+
appendAuditEntry({ actor: actor ?? 'unknown', ...(actorId ? { actor_id: actorId } : {}), action: 'create', item_id: result.id, item_type: entity }, targetCwd);
|
|
5876
6392
|
return {
|
|
5877
6393
|
response: toolResponse({
|
|
5878
6394
|
content: [{ type: 'text', text: `✔ created ${entity} ${result.id}` }],
|
|
@@ -5927,6 +6443,13 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5927
6443
|
if (name === 'bclaw_transition') {
|
|
5928
6444
|
try {
|
|
5929
6445
|
const entity = String(args.entity ?? '');
|
|
6446
|
+
// Same signaling-only boundary as bclaw_create: no remote lifecycle
|
|
6447
|
+
// driving of execution entities through the canonical grammar.
|
|
6448
|
+
if (entity === 'claim' || entity === 'plan') {
|
|
6449
|
+
const crossProjectError = blockCrossProjectExecution(entity, args);
|
|
6450
|
+
if (crossProjectError)
|
|
6451
|
+
return { response: crossProjectError };
|
|
6452
|
+
}
|
|
5930
6453
|
const id = String(args.id ?? '');
|
|
5931
6454
|
const to = String(args.to ?? '');
|
|
5932
6455
|
const reason = args.reason;
|