brainclaw 1.5.5 → 1.7.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 +5 -4
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +124 -7
- package/dist/commands/bootstrap-loop.js +206 -0
- package/dist/commands/loop.js +156 -0
- package/dist/commands/loops-handlers.js +110 -55
- package/dist/commands/mcp-read-handlers.js +37 -0
- package/dist/commands/mcp-schemas.generated.js +33 -0
- package/dist/commands/mcp.js +661 -207
- package/dist/commands/questions.js +180 -0
- package/dist/commands/reply.js +190 -0
- package/dist/commands/session-end.js +105 -3
- package/dist/commands/session-start.js +32 -53
- package/dist/commands/setup.js +64 -13
- package/dist/commands/switch.js +17 -1
- package/dist/core/agent-capability.js +19 -0
- package/dist/core/agent-files.js +86 -0
- package/dist/core/agent-integrations.js +34 -0
- package/dist/core/agent-inventory.js +27 -0
- package/dist/core/agentrun-reconciler.js +130 -0
- package/dist/core/ai-agent-detection.js +17 -1
- package/dist/core/claims.js +54 -3
- package/dist/core/dirty-scope.js +242 -0
- package/dist/core/dispatch-status.js +219 -0
- package/dist/core/entity-operations.js +128 -9
- package/dist/core/execution-adapters.js +38 -2
- package/dist/core/facade-schema.js +71 -0
- package/dist/core/federation-cloud.js +27 -12
- package/dist/core/federation-materialize.js +57 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/loops/bootstrap-acquire.js +195 -0
- package/dist/core/loops/facade-schema.js +68 -1
- package/dist/core/loops/hooks/bootstrap-write.js +144 -0
- package/dist/core/loops/hooks/notify-operator.js +148 -0
- package/dist/core/loops/hooks/survey-source-reader.js +256 -0
- package/dist/core/loops/index.js +8 -2
- package/dist/core/loops/next-expected.js +63 -0
- package/dist/core/loops/presets/bootstrap.js +75 -0
- package/dist/core/loops/presets/index.js +16 -0
- package/dist/core/loops/store.js +224 -4
- package/dist/core/loops/types.js +346 -1
- package/dist/core/loops/verbs.js +739 -6
- package/dist/core/schema.js +30 -2
- package/dist/core/state.js +62 -0
- package/dist/core/worktree.js +58 -0
- package/dist/facts.js +360 -5
- package/dist/facts.json +359 -4
- package/docs/cli.md +10 -7
- package/docs/concepts/dispatch-lifecycle.md +228 -0
- package/docs/concepts/loop-engine.md +55 -0
- package/docs/concepts/multi-agent-workflows.md +167 -166
- package/docs/concepts/troubleshooting.md +36 -2
- package/docs/index.md +4 -3
- package/docs/integrations/hermes.md +78 -0
- package/docs/integrations/overview.md +22 -18
- package/docs/mcp-schema-changelog.md +8 -1
- package/docs/quickstart-existing-project.md +1 -1
- package/package.json +5 -4
package/dist/commands/mcp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { buildClaimEnvPrefix } from '../core/execution-profile.js';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -11,7 +12,7 @@ import { buildContext, renderContextMarkdown, renderContextPromptTemplate, rende
|
|
|
11
12
|
import { buildCoordinationSnapshot } from '../core/coordination.js';
|
|
12
13
|
import { checkBrainclawInstallableUpdate, getInstalledBrainclawVersion, readDiskBrainclawVersion, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
|
|
13
14
|
import { loadConfig } from '../core/config.js';
|
|
14
|
-
import { loadState, persistState, saveState } from '../core/state.js';
|
|
15
|
+
import { collectLoadValidationWarnings, findLoadValidationWarning, loadState, persistState, saveState } from '../core/state.js';
|
|
15
16
|
import { generateIdWithLabel } from '../core/ids.js';
|
|
16
17
|
import { memoryExists } from '../core/io.js';
|
|
17
18
|
import { generateCandidateIdWithLabel, loadCandidate, saveCandidate } from '../core/candidates.js';
|
|
@@ -35,10 +36,11 @@ import { buildOperationalIdentity, loadAllSessions, loadSessionById } from '../c
|
|
|
35
36
|
import { validateMcpInput, validateMcpField } from '../core/input-validation.js';
|
|
36
37
|
import { createCapability, createTool as createRegistryTool } from '../core/registries.js';
|
|
37
38
|
import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
38
|
-
import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
|
|
39
|
+
import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, getDetectedSetupAgentNames, getInstalledAgentNames, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
|
|
40
|
+
import { buildAgentInventory } from '../core/agent-inventory.js';
|
|
39
41
|
import { resolveEffectiveCwd, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
|
|
40
42
|
import { probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview } from '../core/setup-flow.js';
|
|
41
|
-
import { ensureUserStore } from '../core/setup-state.js';
|
|
43
|
+
import { ensureUserStore, resolveHomeDir } from '../core/setup-state.js';
|
|
42
44
|
import { createPlan, addStep as addStepOp, completeStep as completeStepOp, updateStep as updateStepOp, deleteStep as deleteStepOp, deletePlan as deletePlanOp } from '../core/operations/plan.js';
|
|
43
45
|
import { sendMessage, ackMessage, countActionable, getThread, hasActiveAssignment } from '../core/messaging.js';
|
|
44
46
|
import { dispatch, dispatchReview, generateDispatchBrief } from '../core/dispatcher.js';
|
|
@@ -48,6 +50,7 @@ import { WorkRequestSchema, CoordinateRequestSchema } from '../core/facade-schem
|
|
|
48
50
|
import { getSpawnableAgents, getCapabilityProfile, buildInvokeCommand, validateAgentForDispatch } from '../core/agent-capability.js';
|
|
49
51
|
import { attemptExecution } from '../core/execution.js';
|
|
50
52
|
import { createAgentRun, transitionAgentRun } from '../core/agentruns.js';
|
|
53
|
+
import { sweepDeadPidRunningAgentRunsAtRead } from '../core/agentrun-reconciler.js';
|
|
51
54
|
import { createAssignment, generateAssignmentId, patchAssignmentMessageId, transitionAssignment, bumpActiveAssignmentHeartbeat, } from '../core/assignments.js';
|
|
52
55
|
import { harvestCandidates } from './harvest.js';
|
|
53
56
|
export const SCHEMA_VERSION = '1.0.0';
|
|
@@ -402,11 +405,25 @@ export const MCP_READ_TOOLS = [
|
|
|
402
405
|
required: ['thread_id'],
|
|
403
406
|
},
|
|
404
407
|
},
|
|
408
|
+
{
|
|
409
|
+
name: 'bclaw_dispatch_status',
|
|
410
|
+
description: 'Consolidated dispatch status — given a `target_id` (asgn_/clm_/lop_/run_), resolves all linked entities (assignment, claim, loop, agent_run), reads the on-disk artefacts (brief-ack sentinel + per-assignment stdout/stderr log tails), checks OS pid liveness, and returns a single health verdict + a recommended next action. Replaces the five separate `bclaw_find` / `bclaw_get` calls a caller would otherwise make to verify a dispatch is actually doing useful work. Particularly useful right after `bclaw_coordinate` returns `execution_status="delivered_and_started"` — that response\'s `verify_with` hint points at this tool by name. See docs/concepts/dispatch-lifecycle.md for the full entity model and FSM details.',
|
|
411
|
+
annotations: { tier: 'facade', category: 'coordination', headlessApproval: 'auto' },
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: 'object',
|
|
414
|
+
properties: {
|
|
415
|
+
target_id: { type: 'string', description: 'Any one of: an assignment id (`asgn_…`), a claim id (`clm_…`), a loop id (`lop_…`), or an agent_run id (`run_…`). The tool resolves to the assignment scope internally and fetches the rest.' },
|
|
416
|
+
tail_log_lines: { type: 'number', description: 'How many trailing lines of each captured log file (stdout / stderr) to include in the response. Default 20. Pass 0 to omit tails and only return size_bytes.' },
|
|
417
|
+
stall_threshold_ms: { type: 'number', description: 'Age in ms past which a `running` agent_run with a live pid but no recent activity is considered `stalled`. Default 300000 (5 min).' },
|
|
418
|
+
},
|
|
419
|
+
required: ['target_id'],
|
|
420
|
+
},
|
|
421
|
+
},
|
|
405
422
|
];
|
|
406
423
|
const MCP_WRITE_TOOLS = [
|
|
407
424
|
{
|
|
408
425
|
name: 'bclaw_dispatch',
|
|
409
|
-
description: 'Unified dispatch entry for sequence-lane parallelization. `intent` discriminator: analysis (sequence lane status, read-only), execute (default — analyze + generate briefs + send), review (routes an EXISTING
|
|
426
|
+
description: 'Unified dispatch entry for sequence-lane parallelization (parallelize plans across lanes). To open a NEW review of a commit/branch, use `bclaw_coordinate(intent="review", open_loop=true, targetAgents=[…])` instead — bclaw_dispatch is for sequence-driven execution, NOT for opening new reviews. `intent` discriminator: analysis (sequence lane status, read-only), execute (default — analyze + generate briefs + send to agents), review (routes an EXISTING already-reflected handoff to a reviewer — only for handoffs produced by `session-end --reflect-handoff` or similar). Consolidates the legacy bclaw_dispatch_analysis / bclaw_dispatch / bclaw_dispatch_review. Returns FacadeResponse; for verification semantics see the same response-validation guidance documented on `bclaw_coordinate`.',
|
|
410
427
|
annotations: { tier: 'facade', category: 'coordination', headlessApproval: 'prompt' },
|
|
411
428
|
inputSchema: {
|
|
412
429
|
type: 'object',
|
|
@@ -482,6 +499,21 @@ const MCP_WRITE_TOOLS = [
|
|
|
482
499
|
},
|
|
483
500
|
},
|
|
484
501
|
},
|
|
502
|
+
{
|
|
503
|
+
name: 'bclaw_init_project',
|
|
504
|
+
description: "Initialize brainclaw at an arbitrary path AND register it as a cross_project_link in the caller's store. Lets an agent operating in workspace A bootstrap a brainclaw project in folder B in one MCP call.",
|
|
505
|
+
annotations: { tier: 'standard', category: 'session', headlessApproval: 'prompt' },
|
|
506
|
+
inputSchema: {
|
|
507
|
+
type: 'object',
|
|
508
|
+
properties: {
|
|
509
|
+
path: { type: 'string', description: 'Absolute or relative path of the target folder. Resolved via path.resolve(callerCwd, path).' },
|
|
510
|
+
force: { type: 'boolean', description: 'Pass --force to init (rebuild managed config). Default false.' },
|
|
511
|
+
project_mode: { type: 'string', description: 'Optional project mode (single-project, multi-project, auto).' },
|
|
512
|
+
link_as: { type: 'string', description: 'Optional name to register the cross_project_link under. Defaults to path basename.' },
|
|
513
|
+
},
|
|
514
|
+
required: ['path'],
|
|
515
|
+
},
|
|
516
|
+
},
|
|
485
517
|
{
|
|
486
518
|
name: 'bclaw_write_note',
|
|
487
519
|
description: 'Add a runtime note. Requires contributor trust level or above. Use crossProject to push a runtime-note signal to a linked project (requires role: publisher in cross_project_links config).',
|
|
@@ -630,23 +662,33 @@ const MCP_WRITE_TOOLS = [
|
|
|
630
662
|
},
|
|
631
663
|
{
|
|
632
664
|
name: 'bclaw_add_step',
|
|
633
|
-
description: 'Add a sub-step to a plan item. Requires contributor trust level or above.',
|
|
665
|
+
description: 'Add a sub-step to a plan item. Canonical shape is `{ planId, data: { text, title?, assignee? } }`; legacy top-level `{ text, assignee }` still works for backward compatibility. If both are present, data.* wins and a warning is emitted. Requires contributor trust level or above. Pass `project` to target a step in a plan that lives in a linked project (same pattern as the canonical-grammar tools).',
|
|
634
666
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
635
667
|
inputSchema: {
|
|
636
668
|
type: 'object',
|
|
637
669
|
properties: {
|
|
638
670
|
planId: { type: 'string', description: 'Plan item ID.' },
|
|
639
|
-
|
|
671
|
+
data: {
|
|
672
|
+
type: 'object',
|
|
673
|
+
description: 'Canonical step payload: { text, title?, assignee? }. title is accepted as an alias for text.',
|
|
674
|
+
properties: {
|
|
675
|
+
text: { type: 'string', description: 'Step description.' },
|
|
676
|
+
title: { type: 'string', description: 'Alias for text.' },
|
|
677
|
+
assignee: { type: 'string', description: 'Optional assignee.' },
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
text: { type: 'string', description: 'Legacy top-level step description; prefer data.text.' },
|
|
640
681
|
agent: { type: 'string', description: 'Agent name.' },
|
|
641
682
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
642
|
-
assignee: { type: 'string', description: '
|
|
683
|
+
assignee: { type: 'string', description: 'Legacy top-level optional assignee; prefer data.assignee.' },
|
|
684
|
+
project: { type: 'string', description: 'Optional: name (or path/basename) of a linked project to add the step in. Defaults to the current project. Same resolution as canonical-grammar tools — accepts cross_project_links and workspace store-chain children.' },
|
|
643
685
|
},
|
|
644
|
-
required: ['planId'
|
|
686
|
+
required: ['planId'],
|
|
645
687
|
},
|
|
646
688
|
},
|
|
647
689
|
{
|
|
648
690
|
name: 'bclaw_complete_step',
|
|
649
|
-
description: 'Mark a plan sub-step as done. Requires contributor trust level or above.',
|
|
691
|
+
description: 'Mark a plan sub-step as done. Requires contributor trust level or above. Pass `project` to operate on a plan in a linked project.',
|
|
650
692
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
651
693
|
inputSchema: {
|
|
652
694
|
type: 'object',
|
|
@@ -655,13 +697,14 @@ const MCP_WRITE_TOOLS = [
|
|
|
655
697
|
stepId: { type: 'string', description: 'Step ID to complete.' },
|
|
656
698
|
agent: { type: 'string', description: 'Agent name.' },
|
|
657
699
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
700
|
+
project: { type: 'string', description: 'Optional: name of a linked project to complete the step in. Defaults to the current project.' },
|
|
658
701
|
},
|
|
659
702
|
required: ['planId', 'stepId'],
|
|
660
703
|
},
|
|
661
704
|
},
|
|
662
705
|
{
|
|
663
706
|
name: 'bclaw_update_step',
|
|
664
|
-
description: 'Update a plan sub-step (status, text, assignee). Supports all step statuses: todo, in_progress, testing, done, blocked. Requires contributor trust level or above.',
|
|
707
|
+
description: 'Update a plan sub-step (status, text, assignee). Supports all step statuses: todo, in_progress, testing, done, blocked. Requires contributor trust level or above. Pass `project` to operate on a plan in a linked project.',
|
|
665
708
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
666
709
|
inputSchema: {
|
|
667
710
|
type: 'object',
|
|
@@ -673,13 +716,14 @@ const MCP_WRITE_TOOLS = [
|
|
|
673
716
|
assignee: { type: 'string', description: 'New assignee (empty string to unassign).' },
|
|
674
717
|
agent: { type: 'string', description: 'Agent name.' },
|
|
675
718
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
719
|
+
project: { type: 'string', description: 'Optional: name of a linked project to update the step in. Defaults to the current project.' },
|
|
676
720
|
},
|
|
677
721
|
required: ['planId', 'stepId'],
|
|
678
722
|
},
|
|
679
723
|
},
|
|
680
724
|
{
|
|
681
725
|
name: 'bclaw_delete_step',
|
|
682
|
-
description: 'Remove a sub-step from a plan. Requires contributor trust level or above.',
|
|
726
|
+
description: 'Remove a sub-step from a plan. Requires contributor trust level or above. Pass `project` to operate on a plan in a linked project.',
|
|
683
727
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'prompt' },
|
|
684
728
|
inputSchema: {
|
|
685
729
|
type: 'object',
|
|
@@ -688,6 +732,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
688
732
|
stepId: { type: 'string', description: 'Step ID to delete.' },
|
|
689
733
|
agent: { type: 'string', description: 'Agent name.' },
|
|
690
734
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
735
|
+
project: { type: 'string', description: 'Optional: name of a linked project to delete the step from. Defaults to the current project.' },
|
|
691
736
|
},
|
|
692
737
|
required: ['planId', 'stepId'],
|
|
693
738
|
},
|
|
@@ -877,6 +922,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
877
922
|
task: { type: 'string', description: 'Optional task description (used as claim description when creating a claim).' },
|
|
878
923
|
messageId: { type: 'string', description: 'Optional message/thread ID for traceability.' },
|
|
879
924
|
contextTarget: { type: 'string', description: 'Optional path passed to bclaw_get_context to filter memory.' },
|
|
925
|
+
project: { type: 'string', description: 'Optional linked project name/path. Routes session, context, claims, audit, and bootstrap probe to that project. Defaults to the current cwd.' },
|
|
880
926
|
agent: { type: 'string', description: 'Agent name.' },
|
|
881
927
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
882
928
|
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 },
|
|
@@ -886,13 +932,13 @@ const MCP_WRITE_TOOLS = [
|
|
|
886
932
|
},
|
|
887
933
|
{
|
|
888
934
|
name: 'bclaw_coordinate',
|
|
889
|
-
description: 'Multi-agent coordination facade: assign tasks to agents (with claims), consult agents (no claim), create a review candidate, open an ideation loop, reroute an active claim to another agent, or summarize a thread. Returns a FacadeResponse with selected_targets, delivery_plan, artifacts, and
|
|
935
|
+
description: 'Multi-agent coordination facade: assign tasks to agents (with claims), consult agents (no claim), create a review candidate, open an ideation loop, reroute an active claim to another agent, or summarize a thread. Returns a FacadeResponse with selected_targets, delivery_plan, artifacts, side_effects, and execution_status. IMPORTANT — execution_status semantics: `delivered_and_started` means the spawn wrapper touched the brief-ack sentinel (`.brainclaw/coordination/runtime/ack/<assignment_id>.ack`) — NOT that the worker is doing useful work. Spawned workers may still die silently before consuming the brief (cf. trap trp_38f63ea4). To verify a dispatch is actually alive, query `bclaw_find(entity="agent_run", filter={assignment_id})` then check `status==="running"` AND OS-level pid liveness (Windows: `Get-Process -Id <pid>` ; POSIX: `kill -0 <pid>`) AND `last_event_at` within the last few minutes. See docs/concepts/troubleshooting.md §"Inbox messages stuck / brief-ack never arrived" for the diagnostic playbook, and docs/integrations/<agent>.md for per-agent spawn semantics (notably codex.md re sandbox MCP availability).',
|
|
890
936
|
annotations: { tier: 'facade', category: 'coordination', headlessApproval: 'auto' },
|
|
891
937
|
inputSchema: {
|
|
892
938
|
type: 'object',
|
|
893
939
|
properties: {
|
|
894
940
|
intent: { type: 'string', enum: ['assign', 'consult', 'review', 'reroute', 'summarize', 'ideate'], description: 'Coordination intent. "assign" creates a claim per target agent and dispatches the brief. "consult" dispatches without creating claims. "review" creates a review candidate. "ideate" opens an ideation loop with the task as the proposal seed (driver wire-up: pln#492 phase 2.d). "reroute" releases the current claim and reassigns. "summarize" reads a thread and returns a summary.' },
|
|
895
|
-
task: { type: 'string', description: 'Brief or task description delivered to target agents.' },
|
|
941
|
+
task: { type: 'string', description: 'Brief or task description delivered to target agents. WARNING: the brief is delivered to a worker that may be sandboxed and not have brainclaw MCP wired in its session — notably codex spawned with `--sandbox workspace-write`. Briefs that instruct MCP-call protocols (e.g. require the worker to call `bclaw_send_message`, `bclaw_assignment_update`, `bclaw_complete_step`) will hang the worker silently. Prefer file-based protocols (write findings/reply to a markdown file in the worktree + commit fixes directly on the branch) for these agents. The coordinator then harvests the file and lifecycle-closes the assignment itself. See docs/integrations/<agent>.md for per-agent capability matrix.' },
|
|
896
942
|
scope: { type: 'string', description: 'File or feature scope. Used as claim scope for assign/reroute; as thread id for summarize if threadId is absent.' },
|
|
897
943
|
targetAgents: { type: 'array', items: { type: 'string' }, description: 'Agent names to target. If omitted, all spawnable agents are used.' },
|
|
898
944
|
constraints: { type: 'object', description: 'Optional structured constraints passed alongside the brief (e.g. deadline, reviewCriteria).' },
|
|
@@ -903,6 +949,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
903
949
|
agent: { type: 'string', description: 'Caller agent name.' },
|
|
904
950
|
agentId: { type: 'string', description: 'Caller registered agent id.' },
|
|
905
951
|
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`).' },
|
|
952
|
+
allow_dirty: { type: 'boolean', description: 'Override the scope-aware dirty-working-tree guard (trp#371 Tier 2). The guard runs only for worktree-spawning intents (assign/review/reroute) and blocks only when uncommitted files overlap — or cannot be proven disjoint from — the dispatch scope (the worker spawns from HEAD and will not see them). `.brainclaw/` and `.git/` are always excluded. Set true to proceed anyway (the block is downgraded to a warning that lists the overlapping files). Boolean; the string "true"/"false" are also coerced.' },
|
|
953
|
+
ref: { type: 'string', description: 'Optional git ref (commit/branch/tag) for assign/review/reroute: the dispatched worker builds its worktree from this ref instead of HEAD. When set, uncommitted working-tree changes are intentionally out of scope and the dirty guard allows the dispatch. Ignored by consult/ideate/summarize (no worktree).' },
|
|
906
954
|
},
|
|
907
955
|
required: ['intent', 'task'],
|
|
908
956
|
},
|
|
@@ -924,7 +972,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
924
972
|
intent: {
|
|
925
973
|
type: 'string',
|
|
926
974
|
enum: ['open', 'get', 'list', 'turn', 'complete_turn', 'advance', 'add_artifact', 'pause', 'resume', 'close'],
|
|
927
|
-
description: 'Loop lifecycle intent. See docs/concepts/loop-engine.md for semantics.',
|
|
975
|
+
description: 'Loop lifecycle intent. See docs/concepts/loop-engine.md for semantics. ANTI-PATTERN: do NOT call `intent="open"` directly to start a review or ideation loop — it creates the loop structure WITHOUT dispatching the first turn, so the reviewer/participant never receives the work. Use `bclaw_coordinate(intent="review", open_loop=true, targetAgents=[…])` (or `intent="ideate"`) instead — that opens the loop AND dispatches the first turn in one call. `bclaw_loop` is for driving turns inside a loop that was already opened via the coordinate facade (intents: turn, complete_turn, advance, close, etc.).',
|
|
928
976
|
},
|
|
929
977
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
930
978
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -935,7 +983,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
935
983
|
linked: { type: 'object', description: 'Optional top-level plan/sequence refs (open).' },
|
|
936
984
|
stop_condition: { type: 'object', description: 'Optional stop_condition override (open). Composite any/all supported.' },
|
|
937
985
|
mode: { type: 'string', enum: ['asymmetric', 'symmetric'], description: 'Review mode selector for open (review kind only).' },
|
|
938
|
-
status: { type: 'string', description: '
|
|
986
|
+
status: { type: 'string', description: 'For intent="list": filter value (any loop status). For intent="close": target final status — accepted values are `completed` | `cancelled` | `blocked` only (NOT `failed`; map crashed/dead loops to `cancelled` with a `reason`).' },
|
|
939
987
|
include_events: { type: 'boolean', description: 'get: include the event journal in the response.' },
|
|
940
988
|
limit: { type: 'number', description: 'list: max loops returned.' },
|
|
941
989
|
offset: { type: 'number', description: 'list: pagination offset.' },
|
|
@@ -952,6 +1000,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
952
1000
|
reason: { type: 'string', description: 'advance / pause / close: optional reason string.' },
|
|
953
1001
|
expected_version: { type: 'number', description: 'Accepted for RFC compatibility on mutating intents, but not enforced until lock/CAS wiring lands.' },
|
|
954
1002
|
client_request_id: { type: 'string', description: 'Accepted for RFC compatibility on mutating intents, but not enforced until lock/idempotency wiring lands.' },
|
|
1003
|
+
project: { type: 'string', description: 'Optional linked project name/path. Routes loop reads and mutations to that project. Defaults to the current cwd.' },
|
|
955
1004
|
agent: { type: 'string', description: 'Caller agent name.' },
|
|
956
1005
|
agentId: { type: 'string', description: 'Caller registered agent id (enforced for slot-bound auth in complete_turn).' },
|
|
957
1006
|
},
|
|
@@ -1038,13 +1087,13 @@ const MCP_WRITE_TOOLS = [
|
|
|
1038
1087
|
// Promoted to `standard` tier at the v1.0 cut.
|
|
1039
1088
|
{
|
|
1040
1089
|
name: 'bclaw_find',
|
|
1041
|
-
description: 'Canonical list query over a brainclaw entity. Default read filter excludes records with provenance.kind="legacy" and auto_reflect records below 0.6 confidence — override via filter.includeLegacy / filter.minAutoReflectConfidence. Pass `project` to query a linked project instead of the current one.',
|
|
1090
|
+
description: 'Canonical list query over a brainclaw entity. Default read filter excludes records with provenance.kind="legacy" and auto_reflect records below 0.6 confidence — override via filter.includeLegacy / filter.minAutoReflectConfidence. Tag filters accept `tag: string` for one tag or `tags: string[]` for any-match. For entity="agent_run", filters also accept assignment_id, claim_id, and message_id. Pass `project` to query a linked project instead of the current one.',
|
|
1042
1091
|
annotations: { tier: 'standard', category: 'memory', headlessApproval: 'auto' },
|
|
1043
1092
|
inputSchema: {
|
|
1044
1093
|
type: 'object',
|
|
1045
1094
|
properties: {
|
|
1046
1095
|
entity: { type: 'string', description: 'Entity name: plan | decision | constraint | trap | handoff | runtime_note | candidate | claim | action | assignment | agent_run | cross_project_link. Others not yet wired.' },
|
|
1047
|
-
filter: { type: 'object', description: 'Filter keys: status, tag, author, plan_id, limit, offset, includeLegacy (bool, default false), minAutoReflectConfidence (0-1, default 0.6).' },
|
|
1096
|
+
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.' },
|
|
1048
1097
|
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`.' },
|
|
1049
1098
|
},
|
|
1050
1099
|
required: ['entity'],
|
|
@@ -1215,6 +1264,7 @@ export const FACADE_ORDER = [
|
|
|
1215
1264
|
'bclaw_context',
|
|
1216
1265
|
'bclaw_coordinate',
|
|
1217
1266
|
'bclaw_dispatch',
|
|
1267
|
+
'bclaw_dispatch_status',
|
|
1218
1268
|
'bclaw_loop',
|
|
1219
1269
|
'bclaw_setup',
|
|
1220
1270
|
];
|
|
@@ -2345,8 +2395,14 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2345
2395
|
const repos = scanGitRepos(roots);
|
|
2346
2396
|
const selectedRepos = parseRepoSelection(choice, repos, cwd);
|
|
2347
2397
|
const detected = detectAiAgent(env);
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2398
|
+
const installedAgents = getInstalledAgentNames(buildAgentInventory(resolveHomeDir(env) ?? os.homedir(), env));
|
|
2399
|
+
const detectedSetupAgents = getDetectedSetupAgentNames(detected?.name, installedAgents);
|
|
2400
|
+
const agentList = ALL_KNOWN_AGENTS.map((a, i) => {
|
|
2401
|
+
const tag = a === detected?.name ? ' ← detected' : installedAgents.includes(a) ? ' ← installed' : '';
|
|
2402
|
+
return ` ${i + 1}) ${a}${tag}`;
|
|
2403
|
+
}).join('\n');
|
|
2404
|
+
const detectedLine = detectedSetupAgents.length > 0 ? `\nDetected install set: ${detectedSetupAgents.join(', ')}\n` : '\n';
|
|
2405
|
+
return { response: toolResponse({ content: [{ type: 'text', text: `Selected ${selectedRepos.length} repo(s). Detected AI agent: ${detected?.name ?? 'none'}.${detectedLine}\nAvailable agents:\n${agentList}\n\nAsk the user which agents to configure.` }], structuredContent: { pending_question: 'agent_selection', roots: rootsArg, repo_selection: choice, selected_repos: selectedRepos.map((r) => ({ path: r.path, name: r.name })), detected_agent: detected?.name ?? null, installed_agents: installedAgents, detected_setup_agents: detectedSetupAgents, all_agents: ALL_KNOWN_AGENTS, prompt: 'Please ask the user: "Which agents to configure? Reply: (d)etected installed, (a)ll, or agent names like claude-code,cursor"' } }) };
|
|
2350
2406
|
}
|
|
2351
2407
|
if (step === 'agent_selection') {
|
|
2352
2408
|
if (!rootsArg || !repoSelectionArg) {
|
|
@@ -2356,7 +2412,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2356
2412
|
const repos = scanGitRepos(roots);
|
|
2357
2413
|
const selectedRepos = parseRepoSelection(repoSelectionArg, repos, cwd);
|
|
2358
2414
|
const detected = detectAiAgent(env);
|
|
2359
|
-
const
|
|
2415
|
+
const installedAgents = getInstalledAgentNames(buildAgentInventory(resolveHomeDir(env) ?? os.homedir(), env));
|
|
2416
|
+
const selectedAgents = parseAgentSelection(choice, detected?.name, installedAgents);
|
|
2360
2417
|
const summary = [];
|
|
2361
2418
|
const written = runGlobalInstall(selectedAgents, env);
|
|
2362
2419
|
for (const f of written)
|
|
@@ -2377,6 +2434,102 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2377
2434
|
}
|
|
2378
2435
|
return { response: toolResponse({ content: [{ type: 'text', text: `Unknown step: "${step}". Valid steps: project_roots, repo_selection, agent_selection.` }], structuredContent: { error: 'unknown_step', step } }, true) };
|
|
2379
2436
|
}
|
|
2437
|
+
if (name === 'bclaw_init_project') {
|
|
2438
|
+
const rawPath = typeof args.path === 'string' ? args.path.trim() : '';
|
|
2439
|
+
if (!rawPath) {
|
|
2440
|
+
return { response: createToolErrorResponse('validation_error', 'path is required') };
|
|
2441
|
+
}
|
|
2442
|
+
const force = args.force === true;
|
|
2443
|
+
const projectModeArg = typeof args.project_mode === 'string' ? args.project_mode : undefined;
|
|
2444
|
+
const linkAs = typeof args.link_as === 'string' && args.link_as.trim().length > 0
|
|
2445
|
+
? args.link_as.trim()
|
|
2446
|
+
: undefined;
|
|
2447
|
+
const resolvedPath = path.isAbsolute(rawPath) ? rawPath : path.resolve(cwd, rawPath);
|
|
2448
|
+
let wasAlreadyInitialized = false;
|
|
2449
|
+
if (memoryExists(resolvedPath) && !force) {
|
|
2450
|
+
wasAlreadyInitialized = true;
|
|
2451
|
+
}
|
|
2452
|
+
else {
|
|
2453
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
2454
|
+
try {
|
|
2455
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
2456
|
+
}
|
|
2457
|
+
catch (err) {
|
|
2458
|
+
return {
|
|
2459
|
+
response: createToolErrorResponse('init_project_failed', `Failed to create target directory '${resolvedPath}': ${err instanceof Error ? err.message : String(err)}`),
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
try {
|
|
2464
|
+
const { runInit } = await import('./init.js');
|
|
2465
|
+
await runInit({
|
|
2466
|
+
yes: true,
|
|
2467
|
+
cwd: resolvedPath,
|
|
2468
|
+
force,
|
|
2469
|
+
...(projectModeArg ? { projectMode: projectModeArg } : {}),
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
catch (err) {
|
|
2473
|
+
return {
|
|
2474
|
+
response: createToolErrorResponse('init_project_failed', `runInit failed for '${resolvedPath}': ${err instanceof Error ? err.message : String(err)}`),
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
let projectName;
|
|
2479
|
+
try {
|
|
2480
|
+
projectName = loadConfig(resolvedPath).project_name;
|
|
2481
|
+
}
|
|
2482
|
+
catch {
|
|
2483
|
+
projectName = path.basename(resolvedPath);
|
|
2484
|
+
}
|
|
2485
|
+
let linkName;
|
|
2486
|
+
try {
|
|
2487
|
+
const { addCrossProjectLink } = await import('../core/cross-project.js');
|
|
2488
|
+
const link = addCrossProjectLink({
|
|
2489
|
+
path: resolvedPath,
|
|
2490
|
+
name: linkAs ?? projectName,
|
|
2491
|
+
cwd,
|
|
2492
|
+
force,
|
|
2493
|
+
});
|
|
2494
|
+
linkName = link.name ?? path.basename(resolvedPath);
|
|
2495
|
+
}
|
|
2496
|
+
catch (err) {
|
|
2497
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2498
|
+
// Treat a duplicate link as idempotent success when the caller did
|
|
2499
|
+
// not request --force; the project itself is initialised correctly
|
|
2500
|
+
// and the existing link already points at it.
|
|
2501
|
+
if (/already exists/i.test(message) && !force) {
|
|
2502
|
+
try {
|
|
2503
|
+
const { resolveCrossProjectLinks } = await import('../core/cross-project.js');
|
|
2504
|
+
const existing = resolveCrossProjectLinks(cwd).find((l) => l.absolutePath === resolvedPath || l.path === rawPath);
|
|
2505
|
+
linkName = existing?.name ?? linkAs ?? projectName;
|
|
2506
|
+
}
|
|
2507
|
+
catch {
|
|
2508
|
+
linkName = linkAs ?? projectName;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
else {
|
|
2512
|
+
return {
|
|
2513
|
+
response: createToolErrorResponse('init_project_failed', `Failed to register cross_project_link: ${message}`),
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
const summary = wasAlreadyInitialized
|
|
2518
|
+
? `✔ ${resolvedPath} already initialised; linked as '${linkName}'.`
|
|
2519
|
+
: `✔ Initialised brainclaw at ${resolvedPath} and linked as '${linkName}'.`;
|
|
2520
|
+
return {
|
|
2521
|
+
response: toolResponse({
|
|
2522
|
+
content: [{ type: 'text', text: summary }],
|
|
2523
|
+
structuredContent: {
|
|
2524
|
+
status: 'ok',
|
|
2525
|
+
project_name: projectName,
|
|
2526
|
+
path: resolvedPath,
|
|
2527
|
+
link_id: linkName,
|
|
2528
|
+
was_already_initialized: wasAlreadyInitialized,
|
|
2529
|
+
},
|
|
2530
|
+
}),
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2380
2533
|
if (name === 'bclaw_write_note') {
|
|
2381
2534
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
2382
2535
|
if (resolved.error) {
|
|
@@ -2887,7 +3040,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2887
3040
|
if (resolved.error && resolved.error.kind !== 'identity_error') {
|
|
2888
3041
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
2889
3042
|
}
|
|
2890
|
-
const result = startSession({
|
|
3043
|
+
const result = await startSession({
|
|
2891
3044
|
agent: resolved.identity?.agent_name ?? (typeof args.agent === 'string' ? args.agent : undefined),
|
|
2892
3045
|
agentId: resolved.identity?.agent_id ?? (typeof args.agentId === 'string' ? args.agentId : undefined),
|
|
2893
3046
|
context: args.context,
|
|
@@ -3030,7 +3183,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3030
3183
|
if (resolved.error) {
|
|
3031
3184
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
3032
3185
|
}
|
|
3033
|
-
const result = endSession({
|
|
3186
|
+
const result = await endSession({
|
|
3034
3187
|
session: args.session,
|
|
3035
3188
|
agent: resolved.identity?.agent_name,
|
|
3036
3189
|
agentId: resolved.identity?.agent_id,
|
|
@@ -3575,13 +3728,22 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3575
3728
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
3576
3729
|
}
|
|
3577
3730
|
const stepPlanId = String(args.planId ?? '').trim();
|
|
3578
|
-
const
|
|
3731
|
+
const stepData = args.data && typeof args.data === 'object' && !Array.isArray(args.data)
|
|
3732
|
+
? args.data
|
|
3733
|
+
: {};
|
|
3734
|
+
if ((args.text !== undefined || args.assignee !== undefined) && Object.keys(stepData).length > 0) {
|
|
3735
|
+
console.warn('[brainclaw:warn] bclaw_add_step received legacy top-level fields alongside data.*; using data.* values');
|
|
3736
|
+
}
|
|
3737
|
+
const stepTextRaw = stepData.text ?? stepData.title ?? args.text;
|
|
3738
|
+
const stepText = typeof stepTextRaw === 'string' ? stepTextRaw.trim() : '';
|
|
3739
|
+
const stepAssignee = (stepData.assignee ?? args.assignee);
|
|
3579
3740
|
if (!stepPlanId)
|
|
3580
3741
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
3581
3742
|
if (!stepText)
|
|
3582
|
-
return { response: createToolErrorResponse('validation_error', 'Missing required argument: text') };
|
|
3743
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: data.text') };
|
|
3744
|
+
const stepTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3583
3745
|
try {
|
|
3584
|
-
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee:
|
|
3746
|
+
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee: stepAssignee }, stepTargetCwd);
|
|
3585
3747
|
return {
|
|
3586
3748
|
response: toolResponse({
|
|
3587
3749
|
content: [{ type: 'text', text: `✔ Step added: [${result.stepId}] ${stepText} (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
@@ -3614,8 +3776,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3614
3776
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
3615
3777
|
if (!csStepId)
|
|
3616
3778
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
|
|
3779
|
+
const csTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3617
3780
|
try {
|
|
3618
|
-
const result = completeStepOp({ planId: csPlanId, stepId: csStepId },
|
|
3781
|
+
const result = completeStepOp({ planId: csPlanId, stepId: csStepId }, csTargetCwd);
|
|
3619
3782
|
return {
|
|
3620
3783
|
response: toolResponse({
|
|
3621
3784
|
content: [{ type: 'text', text: `✔ Step completed: [${result.stepId}] (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
@@ -3653,6 +3816,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3653
3816
|
if (args.status && !validStatuses.includes(String(args.status))) {
|
|
3654
3817
|
return { response: createToolErrorResponse('validation_error', `Invalid status: ${args.status}. Valid: ${validStatuses.join(', ')}`) };
|
|
3655
3818
|
}
|
|
3819
|
+
const usTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3656
3820
|
try {
|
|
3657
3821
|
const result = updateStepOp({
|
|
3658
3822
|
planId: usPlanId,
|
|
@@ -3660,7 +3824,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3660
3824
|
status: args.status,
|
|
3661
3825
|
text: args.text,
|
|
3662
3826
|
assignee: args.assignee,
|
|
3663
|
-
},
|
|
3827
|
+
}, usTargetCwd);
|
|
3664
3828
|
const changes = [];
|
|
3665
3829
|
if (args.status)
|
|
3666
3830
|
changes.push(`status=${args.status}`);
|
|
@@ -3701,8 +3865,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3701
3865
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
3702
3866
|
if (!dsStepId)
|
|
3703
3867
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
|
|
3868
|
+
const dsTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3704
3869
|
try {
|
|
3705
|
-
const result = deleteStepOp({ planId: dsPlanId, stepId: dsStepId },
|
|
3870
|
+
const result = deleteStepOp({ planId: dsPlanId, stepId: dsStepId }, dsTargetCwd);
|
|
3706
3871
|
return {
|
|
3707
3872
|
response: toolResponse({
|
|
3708
3873
|
content: [{ type: 'text', text: `✔ Step deleted: [${result.stepId}] (${result.doneSteps}/${result.totalSteps} remaining)` }],
|
|
@@ -4075,16 +4240,17 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4075
4240
|
return { response: createToolErrorResponse('validation_error', parseResult.error.message) };
|
|
4076
4241
|
}
|
|
4077
4242
|
const workReq = parseResult.data;
|
|
4243
|
+
const targetCwd = resolveProjectCwd(workReq.project, cwd);
|
|
4078
4244
|
const useCompact = workReq.compact !== false; // default true
|
|
4079
4245
|
const warnings = [];
|
|
4080
4246
|
// Step 1: implicit session start (handles auto-registration internally)
|
|
4081
4247
|
let sessionResult;
|
|
4082
4248
|
try {
|
|
4083
|
-
sessionResult = startSession({
|
|
4249
|
+
sessionResult = await startSession({
|
|
4084
4250
|
agent: typeof args.agent === 'string' ? args.agent : undefined,
|
|
4085
4251
|
agentId: typeof args.agentId === 'string' ? args.agentId : undefined,
|
|
4086
4252
|
context: workReq.contextTarget,
|
|
4087
|
-
cwd,
|
|
4253
|
+
cwd: targetCwd,
|
|
4088
4254
|
});
|
|
4089
4255
|
}
|
|
4090
4256
|
catch (sessionErr) {
|
|
@@ -4102,14 +4268,14 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4102
4268
|
try {
|
|
4103
4269
|
let sinceSession;
|
|
4104
4270
|
if (workReq.intent === 'resume') {
|
|
4105
|
-
const previousSession = loadAllSessions(
|
|
4271
|
+
const previousSession = loadAllSessions(targetCwd)
|
|
4106
4272
|
.find((s) => s.agent === sessionResult.agent && s.session_id !== sessionResult.session_id);
|
|
4107
4273
|
sinceSession = previousSession?.session_id;
|
|
4108
4274
|
}
|
|
4109
4275
|
contextResult = buildContext({
|
|
4110
4276
|
target: workReq.contextTarget ?? workReq.scope,
|
|
4111
4277
|
agent: sessionResult.agent,
|
|
4112
|
-
cwd,
|
|
4278
|
+
cwd: targetCwd,
|
|
4113
4279
|
sinceSession,
|
|
4114
4280
|
});
|
|
4115
4281
|
}
|
|
@@ -4118,7 +4284,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4118
4284
|
let claimId;
|
|
4119
4285
|
let claimStatus = 'none';
|
|
4120
4286
|
if (workReq.intent === 'execute' && workReq.scope) {
|
|
4121
|
-
const existingClaims = listClaims(
|
|
4287
|
+
const existingClaims = listClaims(targetCwd).filter((c) => c.status === 'active' && c.agent === sessionResult.agent && c.scope === workReq.scope);
|
|
4122
4288
|
if (existingClaims.length > 0) {
|
|
4123
4289
|
claimId = existingClaims[0].id;
|
|
4124
4290
|
claimStatus = 'existing';
|
|
@@ -4139,11 +4305,11 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4139
4305
|
status: 'active',
|
|
4140
4306
|
plan_id: workReq.planId,
|
|
4141
4307
|
model: currentModel,
|
|
4142
|
-
},
|
|
4143
|
-
appendAuditEntry({ actor: sessionResult.agent, actor_id: sessionResult.agent_id, action: 'claim', item_id: claimId, item_type: 'claim', scope: workReq.scope, session_id: sessionResult.session_id },
|
|
4308
|
+
}, targetCwd);
|
|
4309
|
+
appendAuditEntry({ actor: sessionResult.agent, actor_id: sessionResult.agent_id, action: 'claim', item_id: claimId, item_type: 'claim', scope: workReq.scope, session_id: sessionResult.session_id }, targetCwd);
|
|
4144
4310
|
claimStatus = 'created';
|
|
4145
4311
|
// Policy check post-claim
|
|
4146
|
-
const policyResult = checkPolicy({ scope: workReq.scope, agent: sessionResult.agent, agentId: sessionResult.agent_id, cwd });
|
|
4312
|
+
const policyResult = checkPolicy({ scope: workReq.scope, agent: sessionResult.agent, agentId: sessionResult.agent_id, cwd: targetCwd });
|
|
4147
4313
|
for (const w of policyResult.warnings.filter((pw) => pw.kind !== 'no_claim')) {
|
|
4148
4314
|
const idLabel = w.id ? ` (${w.id})` : '';
|
|
4149
4315
|
warnings.push(`[${w.kind}]${idLabel} ${w.message}`);
|
|
@@ -4185,6 +4351,34 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4185
4351
|
_full_context_hint: 'Use bclaw_context(kind="memory") for the full payload.',
|
|
4186
4352
|
};
|
|
4187
4353
|
}
|
|
4354
|
+
// pln#513 step 1 — bootstrap hint. When the project lacks a usable
|
|
4355
|
+
// PROJECT.md (absent or 0 bytes), surface a hint so the agent knows
|
|
4356
|
+
// the canonical entry-point. Cheap probe — one fs.statSync, no
|
|
4357
|
+
// gating flag. The literal next_action mirrors the documented
|
|
4358
|
+
// canonical-grammar call so callers can suggest it verbatim.
|
|
4359
|
+
let bootstrapRecommended;
|
|
4360
|
+
let nextAction;
|
|
4361
|
+
try {
|
|
4362
|
+
const fsMod = await import('node:fs');
|
|
4363
|
+
const pathMod = await import('node:path');
|
|
4364
|
+
const projectMdPath = pathMod.join(targetCwd, 'PROJECT.md');
|
|
4365
|
+
let needsBootstrap = false;
|
|
4366
|
+
try {
|
|
4367
|
+
const stat = fsMod.statSync(projectMdPath);
|
|
4368
|
+
if (!stat.isFile() || stat.size === 0)
|
|
4369
|
+
needsBootstrap = true;
|
|
4370
|
+
}
|
|
4371
|
+
catch {
|
|
4372
|
+
needsBootstrap = true; // ENOENT → absent
|
|
4373
|
+
}
|
|
4374
|
+
bootstrapRecommended = needsBootstrap;
|
|
4375
|
+
if (needsBootstrap) {
|
|
4376
|
+
nextAction = "bclaw_coordinate(intent='ideate', preset='bootstrap')";
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
catch {
|
|
4380
|
+
// Best-effort: never block bclaw_work on the probe.
|
|
4381
|
+
}
|
|
4188
4382
|
const facadeResponse = {
|
|
4189
4383
|
status: 'ok',
|
|
4190
4384
|
intent: workReq.intent,
|
|
@@ -4195,12 +4389,17 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4195
4389
|
session_id: sessionResult.session_id,
|
|
4196
4390
|
warnings,
|
|
4197
4391
|
duration_ms: Date.now() - startMs,
|
|
4392
|
+
bootstrap_recommended: bootstrapRecommended,
|
|
4393
|
+
next_action: nextAction,
|
|
4198
4394
|
};
|
|
4199
4395
|
const summaryParts = [`✔ bclaw_work [${workReq.intent}] session=${sessionResult.session_id}`];
|
|
4200
4396
|
if (claimId)
|
|
4201
4397
|
summaryParts.push(`claim=${claimId} (${claimStatus})`);
|
|
4202
4398
|
if (useCompact)
|
|
4203
4399
|
summaryParts.push('mode=compact (use bclaw_context for full payload)');
|
|
4400
|
+
if (bootstrapRecommended) {
|
|
4401
|
+
summaryParts.push(`💡 PROJECT.md missing — call ${nextAction} to open a bootstrap loop`);
|
|
4402
|
+
}
|
|
4204
4403
|
if (warnings.length > 0)
|
|
4205
4404
|
summaryParts.push(warnings.map((w) => `⚠ ${w}`).join('\n'));
|
|
4206
4405
|
return {
|
|
@@ -4218,15 +4417,69 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4218
4417
|
return { response: createToolErrorResponse('validation_error', parseResult.error.message) };
|
|
4219
4418
|
}
|
|
4220
4419
|
const req = parseResult.data;
|
|
4420
|
+
// pln#511 step 2 — preset selector validation. Presets are kind-
|
|
4421
|
+
// specific in v1: only intent='ideate' carries them. Unknown names
|
|
4422
|
+
// are rejected up-front against the registry so the handler never
|
|
4423
|
+
// silently falls back to the kind-default. The bootstrap preset
|
|
4424
|
+
// also enforces a dispatch constraint (can_753a083a): the champion
|
|
4425
|
+
// must be a human-connected agent, never a sandboxed worker —
|
|
4426
|
+
// checked below once the senderAgent is resolved.
|
|
4427
|
+
if (req.preset !== undefined) {
|
|
4428
|
+
if (req.intent !== 'ideate') {
|
|
4429
|
+
return {
|
|
4430
|
+
response: createToolErrorResponse('preset_kind_mismatch', `preset='${req.preset}' is only valid for intent='ideate' in v1; got intent='${req.intent}'. Loop presets are kind-specific — open the loop without a preset, or call with intent='ideate'.`),
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4433
|
+
const { PRESETS: PRESETS_REGISTRY } = await import('../core/loops/presets/index.js');
|
|
4434
|
+
if (!(req.preset in PRESETS_REGISTRY)) {
|
|
4435
|
+
const validNames = Object.keys(PRESETS_REGISTRY).join(', ') || '(none)';
|
|
4436
|
+
return {
|
|
4437
|
+
response: createToolErrorResponse('unknown_preset', `Unknown preset '${req.preset}'. Valid preset names: ${validNames}.`),
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
// can_30c295b4 / trp#371 Tier 2 — the scope-aware dirty-working-tree
|
|
4442
|
+
// guard runs LOWER DOWN, after dispatchCwd / isCrossProject are
|
|
4443
|
+
// resolved (so it probes the dispatch TARGET, not the source, and only
|
|
4444
|
+
// for the intents that actually spawn a worktree worker). See the
|
|
4445
|
+
// assessDirtyDispatchGuard call after the cross-project block.
|
|
4221
4446
|
const warnings = [];
|
|
4222
4447
|
const artifacts = [];
|
|
4223
4448
|
const side_effects = [];
|
|
4449
|
+
// can_5e62334e — codex sandboxed dispatches cannot commit in worktrees
|
|
4450
|
+
// because `.git` is a file pointer to the parent repo's
|
|
4451
|
+
// .git/worktrees/<wt>/ directory, which lives OUTSIDE the worktree's
|
|
4452
|
+
// writable root that `--sandbox workspace-write` permits. Any
|
|
4453
|
+
// codex worker that runs `git commit` will fail with `index.lock:
|
|
4454
|
+
// Permission denied`. Warn callers up-front so briefs don't request
|
|
4455
|
+
// per-bug commits; the coordinator must harvest the worktree
|
|
4456
|
+
// diff via `git diff` and commit from a non-sandboxed cwd.
|
|
4457
|
+
if (Array.isArray(req.targetAgents) && req.targetAgents.includes('codex')) {
|
|
4458
|
+
warnings.push('codex --sandbox workspace-write cannot commit to git in worktrees (.git is outside writable root). Briefs MUST NOT request per-bug commits; codex will produce uncommitted edits, then the coordinator must harvest via `git diff HEAD` from the worktree path and commit from the main repo. See can_5e62334e for context.');
|
|
4459
|
+
}
|
|
4224
4460
|
const senderAgent = typeof args.agent === 'string' && args.agent.trim()
|
|
4225
4461
|
? args.agent.trim()
|
|
4226
4462
|
: 'bclaw_coordinate';
|
|
4227
4463
|
const senderAgentId = typeof args.agentId === 'string' && args.agentId.trim()
|
|
4228
4464
|
? args.agentId.trim()
|
|
4229
4465
|
: undefined;
|
|
4466
|
+
// pln#511 step 2 — bootstrap preset dispatch constraint (can_753a083a).
|
|
4467
|
+
// The bootstrap loop's champion must be a human-connected agent: it
|
|
4468
|
+
// asks the operator clarifying questions and writes PROJECT.md. A
|
|
4469
|
+
// sandboxed worker (codex / github-copilot) cannot reach the human,
|
|
4470
|
+
// so the loop would stall in `clarify`. Enforce by requiring
|
|
4471
|
+
// targetAgents to be empty (= single-agent / self-champion mode)
|
|
4472
|
+
// or to contain only the caller. Other presets are unrestricted.
|
|
4473
|
+
if (req.preset === 'bootstrap') {
|
|
4474
|
+
const targets = req.targetAgents ?? [];
|
|
4475
|
+
const onlyCaller = targets.length === 0
|
|
4476
|
+
|| (targets.length === 1 && targets[0] === senderAgent);
|
|
4477
|
+
if (!onlyCaller) {
|
|
4478
|
+
return {
|
|
4479
|
+
response: createToolErrorResponse('bootstrap_preset_not_dispatchable', `preset='bootstrap' cannot dispatch to other agents (can_753a083a): the champion must be a human-connected agent. Got targetAgents=${JSON.stringify(targets)}; pass an empty array or [${JSON.stringify(senderAgent)}].`),
|
|
4480
|
+
};
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4230
4483
|
const commandHints = [];
|
|
4231
4484
|
const preparedInvokes = [];
|
|
4232
4485
|
// pln#359 phase 1b — cross-project routing. When `project` is set, all
|
|
@@ -4243,6 +4496,43 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4243
4496
|
warnings.push(`cross-project dispatch (project='${req.project}') — auto-spawn disabled; the target agent picks up the brief async via its own bclaw_work.`);
|
|
4244
4497
|
}
|
|
4245
4498
|
const effectiveAutoExecute = isCrossProject ? false : req.autoExecute;
|
|
4499
|
+
// can_30c295b4 / trp#371 Tier 2 — scope-aware dirty-working-tree guard.
|
|
4500
|
+
// Only intents that spawn a worktree worker from HEAD can review/edit
|
|
4501
|
+
// stale code, so consult/ideate/summarize are NOT guarded (no worktree
|
|
4502
|
+
// → nothing to protect). Cross-project dispatch is inbox-only (no local
|
|
4503
|
+
// worktree spawned here) so it is skipped too — the target agent builds
|
|
4504
|
+
// its own worktree later via bclaw_work. The guard compares the dirty
|
|
4505
|
+
// files against the dispatch scope and only blocks when overlap can't be
|
|
4506
|
+
// ruled out; allow_dirty=true downgrades a block to a warning, and an
|
|
4507
|
+
// explicit ref makes working-tree dirt intentionally out of scope.
|
|
4508
|
+
const WORKTREE_SPAWNING_INTENTS = new Set(['assign', 'review', 'reroute']);
|
|
4509
|
+
if (!isCrossProject && WORKTREE_SPAWNING_INTENTS.has(req.intent)) {
|
|
4510
|
+
// Probe with the SAME scope the dispatch will actually claim, so the
|
|
4511
|
+
// resolution mirrors reality (codex r1): assign falls back to the task
|
|
4512
|
+
// text (mcp ~assignScope), reroute to the targeted active claim's scope.
|
|
4513
|
+
let guardScope = req.scope;
|
|
4514
|
+
if (req.intent === 'assign') {
|
|
4515
|
+
guardScope = req.scope ?? req.task;
|
|
4516
|
+
}
|
|
4517
|
+
else if (req.intent === 'reroute' && !req.scope) {
|
|
4518
|
+
guardScope = listClaims(dispatchCwd).find((c) => c.status === 'active')?.scope;
|
|
4519
|
+
}
|
|
4520
|
+
const { assessDirtyDispatchGuard } = await import('../core/dirty-scope.js');
|
|
4521
|
+
const assessment = assessDirtyDispatchGuard({
|
|
4522
|
+
cwd: dispatchCwd,
|
|
4523
|
+
scope: guardScope,
|
|
4524
|
+
allowDirty: req.allow_dirty,
|
|
4525
|
+
checkoutRef: req.ref,
|
|
4526
|
+
});
|
|
4527
|
+
if (assessment.decision === 'block') {
|
|
4528
|
+
return {
|
|
4529
|
+
response: createToolErrorResponse('dirty_working_tree', `${assessment.reason} (cwd: ${dispatchCwd})`),
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
if (assessment.decision === 'warn') {
|
|
4533
|
+
warnings.push(`dirty_working_tree: ${assessment.reason}`);
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4246
4536
|
/** Run E2E execution phase on prepared delivery entries. Returns overall execution status. */
|
|
4247
4537
|
const runCoordinateExecution = async (prepared, opts) => {
|
|
4248
4538
|
let overall = 'inbox_only';
|
|
@@ -4495,6 +4785,10 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4495
4785
|
dispatcherAgent: senderAgent,
|
|
4496
4786
|
sessionId: connectionSessionId,
|
|
4497
4787
|
cwd: dispatchCwd,
|
|
4788
|
+
// createCoordinatorClaim guarantees the worktree reflects this ref
|
|
4789
|
+
// (resets a stale branch / re-points a reused worktree) — see the
|
|
4790
|
+
// worktreeBaseRef invariant there (pln#520 Tier 2).
|
|
4791
|
+
worktreeBaseRef: req.ref,
|
|
4498
4792
|
});
|
|
4499
4793
|
const claimId = claimResult.claimId;
|
|
4500
4794
|
if (claimResult.worktreeWarning) {
|
|
@@ -4735,6 +5029,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4735
5029
|
dispatcherAgent: senderAgent,
|
|
4736
5030
|
sessionId: connectionSessionId,
|
|
4737
5031
|
cwd: dispatchCwd,
|
|
5032
|
+
worktreeBaseRef: req.ref,
|
|
4738
5033
|
});
|
|
4739
5034
|
if (claimResult.worktreeWarning)
|
|
4740
5035
|
out.warnings.push(claimResult.worktreeWarning);
|
|
@@ -4923,6 +5218,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4923
5218
|
dispatcherAgent: senderAgent,
|
|
4924
5219
|
sessionId: connectionSessionId,
|
|
4925
5220
|
cwd: dispatchCwd,
|
|
5221
|
+
worktreeBaseRef: req.ref,
|
|
4926
5222
|
});
|
|
4927
5223
|
newClaimId = rerouteClaimResult.claimId;
|
|
4928
5224
|
if (rerouteClaimResult.worktreeWarning) {
|
|
@@ -5043,186 +5339,318 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5043
5339
|
: messages.map((m, i) => `[${i + 1}] ${m.from} → ${m.to}: ${m.text}`).join('\n');
|
|
5044
5340
|
result = { thread_id: threadId, message_count: messages.length, summary };
|
|
5045
5341
|
}
|
|
5046
|
-
else if (req.intent === 'ideate')
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5342
|
+
else if (req.intent === 'ideate')
|
|
5343
|
+
ideate: {
|
|
5344
|
+
// pln#492 phase 2.c (open + proposal) + 2.d.2 (multi-agent dispatch).
|
|
5345
|
+
// Single-agent mode (no targetAgents): open the loop with the task
|
|
5346
|
+
// as a proposal seed and stop there — the champion drives manually
|
|
5347
|
+
// via bclaw_loop intent='turn' / 'advance'. Multi-agent mode
|
|
5348
|
+
// (targetAgents passed): also advance to critique and dispatch a
|
|
5349
|
+
// turn to each critic slot with a context-filtered, BM25-ranked,
|
|
5350
|
+
// size-capped brief assembled by buildIdeationBrief.
|
|
5351
|
+
//
|
|
5352
|
+
// pln#511 step 2 — when `req.preset` is set, the loop opens with
|
|
5353
|
+
// the preset's phases / stop_condition / protocol instead of the
|
|
5354
|
+
// kind-default ideation chain. Bootstrap preset enforces single-
|
|
5355
|
+
// agent / self-champion mode (validated above), so the multi-
|
|
5356
|
+
// agent dispatch branch never runs for it.
|
|
5357
|
+
//
|
|
5358
|
+
// pln#513 step 2 — labelled block (ideate:) so the bootstrap
|
|
5359
|
+
// join-or-lock path can break out early after assigning result.
|
|
5360
|
+
const loopsModuleRef = await import('../core/loops/index.js');
|
|
5361
|
+
const { openLoop, add_artifact, advance, turn, getLoop, buildIdeationBrief } = loopsModuleRef;
|
|
5362
|
+
const presetSelected = req.preset
|
|
5363
|
+
? (await import('../core/loops/presets/index.js')).PRESETS[req.preset]
|
|
5364
|
+
: undefined;
|
|
5365
|
+
// pln#513 step 2 — bootstrap join-or-lock. The bootstrap preset is
|
|
5366
|
+
// project-singleton: two concurrent callers must converge on the same
|
|
5367
|
+
// loop rather than open duplicates. Strategy:
|
|
5368
|
+
// 1. Find existing bootstrap loop in {open, paused} → join.
|
|
5369
|
+
// 2. Else check for an active coordination claim (someone is
|
|
5370
|
+
// mid-open). Re-find once; surface bootstrap_coordination_in_progress
|
|
5371
|
+
// if still nothing.
|
|
5372
|
+
// 3. Else acquire the lock, fall through to the normal open path,
|
|
5373
|
+
// release the lock on the way out (success or fail).
|
|
5374
|
+
// The lock is opportunistic, not blocking — a fast retry-in-place
|
|
5375
|
+
// not a wait-on-mutex. Keeps the verb short and predictable.
|
|
5376
|
+
// pln#518 step 1 — bootstrap join-or-lock now delegates to the shared
|
|
5377
|
+
// acquireBootstrapLoop helper (src/core/loops/bootstrap-acquire.ts).
|
|
5378
|
+
// Both the CLI and this MCP handler converge on the same singleton
|
|
5379
|
+
// acquire path, eliminating the race where two concurrent callers both
|
|
5380
|
+
// passed the local scan and called openLoop directly.
|
|
5381
|
+
const senderIdentity = ((senderAgentId ? findAgentIdentityById(senderAgentId, dispatchCwd) : undefined)
|
|
5382
|
+
?? findAgentIdentityByName(senderAgent, dispatchCwd)
|
|
5383
|
+
?? ensureAgentRegisteredForDispatch(senderAgent, dispatchCwd));
|
|
5384
|
+
const authorAgentId = senderIdentity?.agent_id ?? senderAgentId;
|
|
5385
|
+
const creatorActor = authorAgentId ?? senderAgent;
|
|
5386
|
+
let bootstrapOpenedLoop;
|
|
5387
|
+
if (req.preset === 'bootstrap') {
|
|
5388
|
+
const { acquireBootstrapLoop, BootstrapCoordinationInProgressError: BcipError } = await import('../core/loops/bootstrap-acquire.js');
|
|
5389
|
+
let acqResult;
|
|
5390
|
+
try {
|
|
5391
|
+
acqResult = acquireBootstrapLoop({
|
|
5392
|
+
actor: senderAgent,
|
|
5393
|
+
agent_id: authorAgentId,
|
|
5394
|
+
created_by: creatorActor,
|
|
5395
|
+
title: req.task.slice(0, 120),
|
|
5396
|
+
goal: req.scope,
|
|
5397
|
+
model: currentModel,
|
|
5398
|
+
}, dispatchCwd);
|
|
5399
|
+
}
|
|
5400
|
+
catch (err) {
|
|
5401
|
+
if (err instanceof BcipError) {
|
|
5402
|
+
return {
|
|
5403
|
+
response: createToolErrorResponse('bootstrap_coordination_in_progress', err.message),
|
|
5404
|
+
};
|
|
5405
|
+
}
|
|
5406
|
+
throw err;
|
|
5407
|
+
}
|
|
5408
|
+
warnings.push(...acqResult.warnings);
|
|
5409
|
+
if (acqResult.action === 'joined') {
|
|
5410
|
+
const jLoop = acqResult.loop;
|
|
5411
|
+
artifacts.push({ type: 'loop', id: jLoop.id });
|
|
5412
|
+
result = {
|
|
5413
|
+
loop_id: jLoop.id,
|
|
5414
|
+
joined_existing: true,
|
|
5415
|
+
current_phase: jLoop.current_phase,
|
|
5416
|
+
status: jLoop.status,
|
|
5417
|
+
mode: 'single_agent',
|
|
5418
|
+
preset: req.preset,
|
|
5419
|
+
};
|
|
5420
|
+
// Skip the rest of the ideate flow — we joined an existing loop.
|
|
5421
|
+
break ideate;
|
|
5422
|
+
}
|
|
5423
|
+
// action === 'opened': helper already called openLoop + released lock.
|
|
5424
|
+
bootstrapOpenedLoop = acqResult.loop;
|
|
5077
5425
|
}
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
const updated = add_artifact({
|
|
5094
|
-
id: loop.id,
|
|
5095
|
-
actor: creatorActor,
|
|
5096
|
-
artifact: {
|
|
5097
|
-
phase: 'proposal',
|
|
5098
|
-
type: 'proposal',
|
|
5099
|
-
body: proposalBody,
|
|
5100
|
-
produced_by: creatorActor,
|
|
5426
|
+
// pln#511 step 2 — bootstrap preset always runs in single-agent
|
|
5427
|
+
// mode: the champion drives the whole loop. Even when the caller
|
|
5428
|
+
// passes targetAgents=[caller] (validated as the only legal non-
|
|
5429
|
+
// empty form), we don't add critic slots and we don't take the
|
|
5430
|
+
// multi-agent dispatch branch. Treating that idiom as "single
|
|
5431
|
+
// agent / self-champion" matches what the constraint check
|
|
5432
|
+
// already enforced upstream.
|
|
5433
|
+
const explicitTargets = Boolean(req.targetAgents
|
|
5434
|
+
&& req.targetAgents.length > 0
|
|
5435
|
+
&& req.preset !== 'bootstrap');
|
|
5436
|
+
const slots = [
|
|
5437
|
+
{
|
|
5438
|
+
role: 'champion',
|
|
5439
|
+
agent: senderAgent,
|
|
5440
|
+
...(authorAgentId ? { agent_id: authorAgentId } : {}),
|
|
5101
5441
|
},
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
catch (err) {
|
|
5111
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5112
|
-
return {
|
|
5113
|
-
response: createToolErrorResponse('ideate_failed', `ideate intent: failed to open ideation loop — ${msg}`),
|
|
5114
|
-
};
|
|
5115
|
-
}
|
|
5116
|
-
// pln#492 phase 2.d.2 — multi-agent dispatch. Skipped in single-
|
|
5117
|
-
// agent mode (the champion drives manually).
|
|
5118
|
-
let dispatchedCritics = 0;
|
|
5119
|
-
let dispatchedPhase = 'proposal';
|
|
5120
|
-
if (explicitTargets) {
|
|
5121
|
-
try {
|
|
5122
|
-
// Build a search-backed BriefMemoryProvider. Maps user-facing
|
|
5123
|
-
// memory categories the brief asks for onto src/core/search.ts
|
|
5124
|
-
// sections (BM25 there). Loop-internal categories
|
|
5125
|
-
// (critique_history / revision_history / synthesis_artifact)
|
|
5126
|
-
// are pulled by the assembler from the thread directly.
|
|
5127
|
-
const searchModule = await import('../core/search.js');
|
|
5128
|
-
const sectionByCategory = {
|
|
5129
|
-
traps: 'traps',
|
|
5130
|
-
decisions: 'decisions',
|
|
5131
|
-
constraints: 'constraints',
|
|
5132
|
-
handoffs: 'handoffs',
|
|
5133
|
-
plans: 'plans',
|
|
5134
|
-
candidates: 'candidates',
|
|
5135
|
-
};
|
|
5136
|
-
const provider = {
|
|
5137
|
-
fetch(category, query, topK) {
|
|
5138
|
-
const section = sectionByCategory[category];
|
|
5139
|
-
if (!section)
|
|
5140
|
-
return [];
|
|
5141
|
-
const results = searchModule.search({
|
|
5142
|
-
query,
|
|
5143
|
-
section,
|
|
5144
|
-
maxResults: topK,
|
|
5145
|
-
cwd: dispatchCwd,
|
|
5146
|
-
includePending: section === 'candidates',
|
|
5147
|
-
});
|
|
5148
|
-
return results.map((r) => ({
|
|
5149
|
-
id: r.id,
|
|
5150
|
-
category,
|
|
5151
|
-
text: r.text,
|
|
5152
|
-
score: r.score,
|
|
5153
|
-
}));
|
|
5154
|
-
},
|
|
5155
|
-
};
|
|
5156
|
-
// Advance proposal → critique. proposal has no advance_gate so
|
|
5157
|
-
// this is unconditional. After advance, the loop sits at the
|
|
5158
|
-
// critique phase ready for critic turns.
|
|
5159
|
-
advance({ id: loopId, actor: creatorActor }, dispatchCwd);
|
|
5160
|
-
const advancedLoop = getLoop(loopId, dispatchCwd);
|
|
5161
|
-
if (!advancedLoop) {
|
|
5162
|
-
throw new Error('ideate dispatch: loop disappeared after advance');
|
|
5163
|
-
}
|
|
5164
|
-
dispatchedPhase = advancedLoop.current_phase;
|
|
5165
|
-
const criticSlots = advancedLoop.slots.filter((s) => s.role === 'critic');
|
|
5166
|
-
for (const slot of criticSlots) {
|
|
5167
|
-
if (!slot.agent)
|
|
5168
|
-
continue;
|
|
5169
|
-
const briefResult = buildIdeationBrief({
|
|
5170
|
-
thread: advancedLoop,
|
|
5171
|
-
slotRole: slot.role,
|
|
5172
|
-
memoryProvider: provider,
|
|
5442
|
+
];
|
|
5443
|
+
if (explicitTargets) {
|
|
5444
|
+
for (const agent of req.targetAgents) {
|
|
5445
|
+
const criticIdentity = findAgentIdentityByName(agent, dispatchCwd) ?? ensureAgentRegisteredForDispatch(agent, dispatchCwd);
|
|
5446
|
+
slots.push({
|
|
5447
|
+
role: 'critic',
|
|
5448
|
+
agent,
|
|
5449
|
+
...(criticIdentity?.agent_id ? { agent_id: criticIdentity.agent_id } : {}),
|
|
5173
5450
|
});
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
let loopId;
|
|
5454
|
+
let proposalArtifactId;
|
|
5455
|
+
if (bootstrapOpenedLoop) {
|
|
5456
|
+
// Bootstrap case: helper already opened the loop and released its lock.
|
|
5457
|
+
loopId = bootstrapOpenedLoop.id;
|
|
5458
|
+
artifacts.push({ type: 'loop', id: bootstrapOpenedLoop.id });
|
|
5459
|
+
side_effects.push({ action: 'create', entity: 'loop', id: bootstrapOpenedLoop.id });
|
|
5460
|
+
}
|
|
5461
|
+
else {
|
|
5462
|
+
try {
|
|
5463
|
+
const loop = openLoop({
|
|
5464
|
+
kind: 'ideation',
|
|
5465
|
+
title: req.task.slice(0, 120),
|
|
5466
|
+
goal: req.scope,
|
|
5467
|
+
created_by: creatorActor,
|
|
5468
|
+
slots,
|
|
5469
|
+
...(presetSelected
|
|
5470
|
+
? {
|
|
5471
|
+
phases: presetSelected.phases,
|
|
5472
|
+
stop_condition: presetSelected.stop_condition,
|
|
5473
|
+
protocol: presetSelected.protocol,
|
|
5474
|
+
}
|
|
5475
|
+
: {}),
|
|
5179
5476
|
}, dispatchCwd);
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5477
|
+
loopId = loop.id;
|
|
5478
|
+
artifacts.push({ type: 'loop', id: loop.id });
|
|
5479
|
+
side_effects.push({ action: 'create', entity: 'loop', id: loop.id });
|
|
5480
|
+
// pln#511 step 2 — skip the proposal-seed artifact when a preset
|
|
5481
|
+
// is in use. The kind-default ideation chain opens at phase
|
|
5482
|
+
// 'proposal' and the seed artifact lives there; presets define
|
|
5483
|
+
// their own initial phase + seeding semantics (bootstrap starts
|
|
5484
|
+
// at 'survey' and produces a signals_report). Forcing a
|
|
5485
|
+
// 'proposal'-phased artifact here would dangle on a phase the
|
|
5486
|
+
// loop doesn't contain. The task text is already captured on
|
|
5487
|
+
// the thread (title + goal).
|
|
5488
|
+
if (!presetSelected) {
|
|
5489
|
+
const proposalBody = req.task.slice(0, 4000);
|
|
5490
|
+
const updated = add_artifact({
|
|
5491
|
+
id: loop.id,
|
|
5492
|
+
actor: creatorActor,
|
|
5493
|
+
artifact: {
|
|
5494
|
+
phase: 'proposal',
|
|
5495
|
+
type: 'proposal',
|
|
5496
|
+
body: proposalBody,
|
|
5497
|
+
produced_by: creatorActor,
|
|
5498
|
+
},
|
|
5499
|
+
}, dispatchCwd);
|
|
5500
|
+
const lastArtifact = updated.artifacts[updated.artifacts.length - 1];
|
|
5501
|
+
proposalArtifactId = lastArtifact?.artifact_id;
|
|
5502
|
+
if (proposalArtifactId) {
|
|
5503
|
+
artifacts.push({ type: 'artifact', id: proposalArtifactId });
|
|
5504
|
+
side_effects.push({ action: 'create', entity: 'artifact', id: proposalArtifactId });
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
catch (err) {
|
|
5509
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5510
|
+
return {
|
|
5511
|
+
response: createToolErrorResponse('ideate_failed', `ideate intent: failed to open ideation loop — ${msg}`),
|
|
5512
|
+
};
|
|
5513
|
+
}
|
|
5514
|
+
} // end else (non-bootstrap open path)
|
|
5515
|
+
// pln#492 phase 2.d.2 — multi-agent dispatch. Skipped in single-
|
|
5516
|
+
// agent mode (the champion drives manually).
|
|
5517
|
+
//
|
|
5518
|
+
// pln#511 step 2 — initial phase comes from the actual loop's
|
|
5519
|
+
// first phase, not a hardcoded 'proposal'. Presets like bootstrap
|
|
5520
|
+
// open at 'survey'; the kind-default ideation chain still opens
|
|
5521
|
+
// at 'proposal', so this is backward compatible.
|
|
5522
|
+
let dispatchedCritics = 0;
|
|
5523
|
+
let dispatchedPhase = presetSelected ? presetSelected.phases[0].name : 'proposal';
|
|
5524
|
+
if (explicitTargets) {
|
|
5525
|
+
try {
|
|
5526
|
+
// Build a search-backed BriefMemoryProvider. Maps user-facing
|
|
5527
|
+
// memory categories the brief asks for onto src/core/search.ts
|
|
5528
|
+
// sections (BM25 there). Loop-internal categories
|
|
5529
|
+
// (critique_history / revision_history / synthesis_artifact)
|
|
5530
|
+
// are pulled by the assembler from the thread directly.
|
|
5531
|
+
const searchModule = await import('../core/search.js');
|
|
5532
|
+
const sectionByCategory = {
|
|
5533
|
+
traps: 'traps',
|
|
5534
|
+
decisions: 'decisions',
|
|
5535
|
+
constraints: 'constraints',
|
|
5536
|
+
handoffs: 'handoffs',
|
|
5537
|
+
plans: 'plans',
|
|
5538
|
+
candidates: 'candidates',
|
|
5539
|
+
};
|
|
5540
|
+
const provider = {
|
|
5541
|
+
fetch(category, query, topK) {
|
|
5542
|
+
const section = sectionByCategory[category];
|
|
5543
|
+
if (!section)
|
|
5544
|
+
return [];
|
|
5545
|
+
const results = searchModule.search({
|
|
5546
|
+
query,
|
|
5547
|
+
section,
|
|
5548
|
+
maxResults: topK,
|
|
5549
|
+
cwd: dispatchCwd,
|
|
5550
|
+
includePending: section === 'candidates',
|
|
5551
|
+
});
|
|
5552
|
+
return results.map((r) => ({
|
|
5553
|
+
id: r.id,
|
|
5554
|
+
category,
|
|
5555
|
+
text: r.text,
|
|
5556
|
+
score: r.score,
|
|
5557
|
+
}));
|
|
5194
5558
|
},
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5559
|
+
};
|
|
5560
|
+
// Advance proposal → critique. proposal has no advance_gate so
|
|
5561
|
+
// this is unconditional. After advance, the loop sits at the
|
|
5562
|
+
// critique phase ready for critic turns.
|
|
5563
|
+
advance({ id: loopId, actor: creatorActor }, dispatchCwd);
|
|
5564
|
+
const advancedLoop = getLoop(loopId, dispatchCwd);
|
|
5565
|
+
if (!advancedLoop) {
|
|
5566
|
+
throw new Error('ideate dispatch: loop disappeared after advance');
|
|
5567
|
+
}
|
|
5568
|
+
dispatchedPhase = advancedLoop.current_phase;
|
|
5569
|
+
const criticSlots = advancedLoop.slots.filter((s) => s.role === 'critic');
|
|
5570
|
+
for (const slot of criticSlots) {
|
|
5571
|
+
if (!slot.agent)
|
|
5572
|
+
continue;
|
|
5573
|
+
const briefResult = buildIdeationBrief({
|
|
5574
|
+
thread: advancedLoop,
|
|
5575
|
+
slotRole: slot.role,
|
|
5576
|
+
memoryProvider: provider,
|
|
5577
|
+
});
|
|
5578
|
+
turn({
|
|
5579
|
+
id: loopId,
|
|
5580
|
+
slot_id: slot.slot_id,
|
|
5581
|
+
actor: creatorActor,
|
|
5582
|
+
input: briefResult.text,
|
|
5583
|
+
}, dispatchCwd);
|
|
5584
|
+
const queued = queueCoordinateMessage({
|
|
5585
|
+
agent: slot.agent,
|
|
5586
|
+
text: briefResult.text,
|
|
5587
|
+
messageType: 'rfc',
|
|
5588
|
+
ref: loopId,
|
|
5589
|
+
scope: req.scope,
|
|
5590
|
+
tags: ['coordinate', 'ideate', 'loop'],
|
|
5591
|
+
payload: {
|
|
5592
|
+
intent: 'ideate',
|
|
5593
|
+
loop_id: loopId,
|
|
5594
|
+
slot_id: slot.slot_id,
|
|
5595
|
+
phase: advancedLoop.current_phase,
|
|
5596
|
+
iteration: advancedLoop.iteration_count,
|
|
5597
|
+
proposal_artifact_id: proposalArtifactId,
|
|
5598
|
+
},
|
|
5599
|
+
commandMode: 'consult',
|
|
5600
|
+
});
|
|
5601
|
+
void queued;
|
|
5602
|
+
dispatchedCritics += 1;
|
|
5603
|
+
if (briefResult.truncated) {
|
|
5604
|
+
warnings.push(`Brief for critic slot ${slot.slot_id} (${slot.agent}) truncated: ${briefResult.droppedItems} memory items dropped to fit cap`);
|
|
5605
|
+
}
|
|
5201
5606
|
}
|
|
5202
5607
|
}
|
|
5608
|
+
catch (err) {
|
|
5609
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5610
|
+
warnings.push(`ideate dispatch failed: ${msg}; loop ${loopId} stays at proposal phase`);
|
|
5611
|
+
facadeStatus = 'partial';
|
|
5612
|
+
}
|
|
5203
5613
|
}
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5614
|
+
if (!explicitTargets) {
|
|
5615
|
+
warnings.push(presetSelected
|
|
5616
|
+
? `ideate single-agent mode (preset='${req.preset}'): loop opened at phase '${dispatchedPhase}'; champion drives manually via bclaw_loop intent='turn' / 'advance'.`
|
|
5617
|
+
: "ideate single-agent mode: loop opened with proposal seed; champion drives manually via bclaw_loop intent='turn' / 'advance'. Pass targetAgents to enable multi-agent auto-dispatch.");
|
|
5208
5618
|
}
|
|
5619
|
+
result = {
|
|
5620
|
+
loop_id: loopId,
|
|
5621
|
+
proposal_artifact_id: proposalArtifactId,
|
|
5622
|
+
selected_targets: explicitTargets ? req.targetAgents : [],
|
|
5623
|
+
mode: explicitTargets ? 'multi_agent' : 'single_agent',
|
|
5624
|
+
dispatched_critics: dispatchedCritics,
|
|
5625
|
+
current_phase: dispatchedPhase,
|
|
5626
|
+
...(presetSelected ? { preset: req.preset } : {}),
|
|
5627
|
+
};
|
|
5628
|
+
// pln#518 step 1 — bootstrap lock is now managed inside acquireBootstrapLoop;
|
|
5629
|
+
// no release needed here.
|
|
5209
5630
|
}
|
|
5210
|
-
if (!explicitTargets) {
|
|
5211
|
-
warnings.push("ideate single-agent mode: loop opened with proposal seed; champion drives manually via bclaw_loop intent='turn' / 'advance'. Pass targetAgents to enable multi-agent auto-dispatch.");
|
|
5212
|
-
}
|
|
5213
|
-
result = {
|
|
5214
|
-
loop_id: loopId,
|
|
5215
|
-
proposal_artifact_id: proposalArtifactId,
|
|
5216
|
-
selected_targets: explicitTargets ? req.targetAgents : [],
|
|
5217
|
-
mode: explicitTargets ? 'multi_agent' : 'single_agent',
|
|
5218
|
-
dispatched_critics: dispatchedCritics,
|
|
5219
|
-
current_phase: dispatchedPhase,
|
|
5220
|
-
};
|
|
5221
|
-
}
|
|
5222
5631
|
// Extract execution_status from result if present (assign/reroute set it)
|
|
5223
5632
|
const resultExecStatus = (result && typeof result === 'object' && 'execution_status' in result)
|
|
5224
5633
|
? result.execution_status
|
|
5225
5634
|
: undefined;
|
|
5635
|
+
// pln#503 phase 3.3: when execution_status === 'delivered_and_started',
|
|
5636
|
+
// attach a self-documenting `verify_with` hint pointing at the assignment
|
|
5637
|
+
// record. Callers should not take delivered_and_started at face value —
|
|
5638
|
+
// it only attests the brief-ack sentinel was touched, not that the worker
|
|
5639
|
+
// is doing useful work. The hint tells them exactly which canonical-
|
|
5640
|
+
// grammar call to make next to verify spawn liveness.
|
|
5641
|
+
let verifyWith;
|
|
5642
|
+
if (resultExecStatus === 'delivered_and_started') {
|
|
5643
|
+
const firstAssignment = artifacts.find((a) => a.type === 'assignment');
|
|
5644
|
+
if (firstAssignment) {
|
|
5645
|
+
verifyWith = {
|
|
5646
|
+
action: 'bclaw_find',
|
|
5647
|
+
entity: 'agent_run',
|
|
5648
|
+
filter: { assignment_id: firstAssignment.id },
|
|
5649
|
+
expected_when_alive: 'agent_run with status="running" AND OS pid alive AND last_event_at within the last few minutes',
|
|
5650
|
+
see_also: 'docs/concepts/dispatch-lifecycle.md',
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5226
5654
|
const facadeResponse = {
|
|
5227
5655
|
status: facadeStatus,
|
|
5228
5656
|
intent: req.intent,
|
|
@@ -5232,6 +5660,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5232
5660
|
warnings,
|
|
5233
5661
|
duration_ms: Date.now() - startMs,
|
|
5234
5662
|
...(resultExecStatus ? { execution_status: resultExecStatus } : {}),
|
|
5663
|
+
...(verifyWith ? { verify_with: verifyWith } : {}),
|
|
5235
5664
|
};
|
|
5236
5665
|
const summaryParts = [`✔ bclaw_coordinate [${req.intent}] targets=${resolvedAgents.length}`];
|
|
5237
5666
|
if (resultExecStatus)
|
|
@@ -5247,7 +5676,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5247
5676
|
}
|
|
5248
5677
|
if (name === 'bclaw_loop') {
|
|
5249
5678
|
const { handleBclawLoop } = await import('./loops-handlers.js');
|
|
5250
|
-
const
|
|
5679
|
+
const targetCwd = resolveProjectCwd(args?.project, cwd);
|
|
5680
|
+
const result = handleBclawLoop({ args: args, cwd: targetCwd });
|
|
5251
5681
|
return {
|
|
5252
5682
|
response: toolResponse({
|
|
5253
5683
|
content: [{ type: 'text', text: result.summary }],
|
|
@@ -5321,10 +5751,12 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5321
5751
|
// applied when it hadn't. Under the new contract, an unknown key is
|
|
5322
5752
|
// a validation_error listing the keys actually honored.
|
|
5323
5753
|
const KNOWN_FILTER_KEYS = new Set([
|
|
5324
|
-
'status', 'tag', 'author', 'plan_id', 'source', 'auto_generated',
|
|
5754
|
+
'status', 'tag', 'tags', 'author', 'plan_id', 'source', 'auto_generated',
|
|
5755
|
+
'assignment_id', 'claim_id', 'message_id',
|
|
5325
5756
|
'limit', 'offset', 'includeLegacy', 'minAutoReflectConfidence',
|
|
5326
5757
|
]);
|
|
5327
|
-
const
|
|
5758
|
+
const agentRunOnlyFilterKeys = new Set(['assignment_id', 'claim_id', 'message_id']);
|
|
5759
|
+
const unknownKeys = Object.keys(filter).filter((k) => !KNOWN_FILTER_KEYS.has(k) || (agentRunOnlyFilterKeys.has(k) && entity !== 'agent_run'));
|
|
5328
5760
|
if (unknownKeys.length > 0) {
|
|
5329
5761
|
return {
|
|
5330
5762
|
response: createToolErrorResponse('validation_error', `Unknown filter key(s): ${unknownKeys.map((k) => `"${k}"`).join(', ')}. ` +
|
|
@@ -5332,6 +5764,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5332
5764
|
};
|
|
5333
5765
|
}
|
|
5334
5766
|
const result = listEntities(entity, targetCwd, filter);
|
|
5767
|
+
const warnings = collectLoadValidationWarnings(entity, targetCwd);
|
|
5335
5768
|
// structuredContent is the canonical MCP return channel that clients
|
|
5336
5769
|
// (VS Code extension, Codex, etc.) read for machine-parseable data.
|
|
5337
5770
|
// Prior to this fix we spread `...result` at top-level of the
|
|
@@ -5341,7 +5774,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5341
5774
|
return {
|
|
5342
5775
|
response: toolResponse({
|
|
5343
5776
|
content: [{ type: 'text', text: `✔ ${result.total} ${entity} item(s)` }],
|
|
5344
|
-
structuredContent: { ...result },
|
|
5777
|
+
structuredContent: { ...result, warnings },
|
|
5345
5778
|
}),
|
|
5346
5779
|
};
|
|
5347
5780
|
}
|
|
@@ -5354,6 +5787,21 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5354
5787
|
const entity = String(args.entity ?? '');
|
|
5355
5788
|
const id = String(args.id ?? '');
|
|
5356
5789
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
5790
|
+
const validationWarning = findLoadValidationWarning(entity, id, targetCwd);
|
|
5791
|
+
if (validationWarning) {
|
|
5792
|
+
return {
|
|
5793
|
+
response: toolResponse({
|
|
5794
|
+
content: [{ type: 'text', text: `✖ ${entity} ${id} failed validation at load` }],
|
|
5795
|
+
structuredContent: {
|
|
5796
|
+
ok: false,
|
|
5797
|
+
error: 'validation_failed',
|
|
5798
|
+
entity_id: validationWarning.entity_id,
|
|
5799
|
+
validation_errors: validationWarning.validation_errors,
|
|
5800
|
+
path: validationWarning.path,
|
|
5801
|
+
},
|
|
5802
|
+
}, true),
|
|
5803
|
+
};
|
|
5804
|
+
}
|
|
5357
5805
|
const item = getEntity(entity, id, targetCwd);
|
|
5358
5806
|
return {
|
|
5359
5807
|
response: toolResponse({
|
|
@@ -5514,7 +5962,7 @@ export async function executeMcpToolCall(payload) {
|
|
|
5514
5962
|
}
|
|
5515
5963
|
else {
|
|
5516
5964
|
// First-ever connection for this claim — start a fresh session + adopt.
|
|
5517
|
-
const sessionResult = startSession({ cwd, maintenanceMode: 'fast' });
|
|
5965
|
+
const sessionResult = await startSession({ cwd, maintenanceMode: 'fast' });
|
|
5518
5966
|
autoSessionId = sessionResult.session_id;
|
|
5519
5967
|
effectiveConnectionSessionId = autoSessionId;
|
|
5520
5968
|
try {
|
|
@@ -5545,6 +5993,12 @@ export async function executeMcpToolCall(payload) {
|
|
|
5545
5993
|
}
|
|
5546
5994
|
catch { /* best-effort */ }
|
|
5547
5995
|
}
|
|
5996
|
+
if ((payload.name === 'bclaw_find' || payload.name === 'bclaw_get') && payload.args.entity === 'agent_run') {
|
|
5997
|
+
try {
|
|
5998
|
+
sweepDeadPidRunningAgentRunsAtRead(cwd);
|
|
5999
|
+
}
|
|
6000
|
+
catch { /* best-effort */ }
|
|
6001
|
+
}
|
|
5548
6002
|
// ── Delegate to inner handler ───────────────────────────────────────────────
|
|
5549
6003
|
const outcome = await _executeMcpToolCallInner({
|
|
5550
6004
|
...payload,
|