brainclaw 1.5.5 → 1.6.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/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.js +621 -202
- 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/switch.js +17 -1
- package/dist/core/agentrun-reconciler.js +65 -0
- package/dist/core/claims.js +29 -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 +55 -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 +28 -2
- package/dist/core/state.js +62 -0
- package/dist/facts.js +7 -5
- package/dist/facts.json +6 -4
- 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 +10 -2
- package/docs/integrations/overview.md +14 -12
- package/package.json +1 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -11,7 +11,7 @@ import { buildContext, renderContextMarkdown, renderContextPromptTemplate, rende
|
|
|
11
11
|
import { buildCoordinationSnapshot } from '../core/coordination.js';
|
|
12
12
|
import { checkBrainclawInstallableUpdate, getInstalledBrainclawVersion, readDiskBrainclawVersion, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
|
|
13
13
|
import { loadConfig } from '../core/config.js';
|
|
14
|
-
import { loadState, persistState, saveState } from '../core/state.js';
|
|
14
|
+
import { collectLoadValidationWarnings, findLoadValidationWarning, loadState, persistState, saveState } from '../core/state.js';
|
|
15
15
|
import { generateIdWithLabel } from '../core/ids.js';
|
|
16
16
|
import { memoryExists } from '../core/io.js';
|
|
17
17
|
import { generateCandidateIdWithLabel, loadCandidate, saveCandidate } from '../core/candidates.js';
|
|
@@ -48,6 +48,7 @@ import { WorkRequestSchema, CoordinateRequestSchema } from '../core/facade-schem
|
|
|
48
48
|
import { getSpawnableAgents, getCapabilityProfile, buildInvokeCommand, validateAgentForDispatch } from '../core/agent-capability.js';
|
|
49
49
|
import { attemptExecution } from '../core/execution.js';
|
|
50
50
|
import { createAgentRun, transitionAgentRun } from '../core/agentruns.js';
|
|
51
|
+
import { sweepDeadPidRunningAgentRunsAtRead } from '../core/agentrun-reconciler.js';
|
|
51
52
|
import { createAssignment, generateAssignmentId, patchAssignmentMessageId, transitionAssignment, bumpActiveAssignmentHeartbeat, } from '../core/assignments.js';
|
|
52
53
|
import { harvestCandidates } from './harvest.js';
|
|
53
54
|
export const SCHEMA_VERSION = '1.0.0';
|
|
@@ -402,11 +403,25 @@ export const MCP_READ_TOOLS = [
|
|
|
402
403
|
required: ['thread_id'],
|
|
403
404
|
},
|
|
404
405
|
},
|
|
406
|
+
{
|
|
407
|
+
name: 'bclaw_dispatch_status',
|
|
408
|
+
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.',
|
|
409
|
+
annotations: { tier: 'facade', category: 'coordination', headlessApproval: 'auto' },
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: 'object',
|
|
412
|
+
properties: {
|
|
413
|
+
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.' },
|
|
414
|
+
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.' },
|
|
415
|
+
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).' },
|
|
416
|
+
},
|
|
417
|
+
required: ['target_id'],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
405
420
|
];
|
|
406
421
|
const MCP_WRITE_TOOLS = [
|
|
407
422
|
{
|
|
408
423
|
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
|
|
424
|
+
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 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
425
|
annotations: { tier: 'facade', category: 'coordination', headlessApproval: 'prompt' },
|
|
411
426
|
inputSchema: {
|
|
412
427
|
type: 'object',
|
|
@@ -482,6 +497,21 @@ const MCP_WRITE_TOOLS = [
|
|
|
482
497
|
},
|
|
483
498
|
},
|
|
484
499
|
},
|
|
500
|
+
{
|
|
501
|
+
name: 'bclaw_init_project',
|
|
502
|
+
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.",
|
|
503
|
+
annotations: { tier: 'standard', category: 'session', headlessApproval: 'prompt' },
|
|
504
|
+
inputSchema: {
|
|
505
|
+
type: 'object',
|
|
506
|
+
properties: {
|
|
507
|
+
path: { type: 'string', description: 'Absolute or relative path of the target folder. Resolved via path.resolve(callerCwd, path).' },
|
|
508
|
+
force: { type: 'boolean', description: 'Pass --force to init (rebuild managed config). Default false.' },
|
|
509
|
+
project_mode: { type: 'string', description: 'Optional project mode (single-project, multi-project, auto).' },
|
|
510
|
+
link_as: { type: 'string', description: 'Optional name to register the cross_project_link under. Defaults to path basename.' },
|
|
511
|
+
},
|
|
512
|
+
required: ['path'],
|
|
513
|
+
},
|
|
514
|
+
},
|
|
485
515
|
{
|
|
486
516
|
name: 'bclaw_write_note',
|
|
487
517
|
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 +660,33 @@ const MCP_WRITE_TOOLS = [
|
|
|
630
660
|
},
|
|
631
661
|
{
|
|
632
662
|
name: 'bclaw_add_step',
|
|
633
|
-
description: 'Add a sub-step to a plan item. Requires contributor trust level or above.',
|
|
663
|
+
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
664
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
635
665
|
inputSchema: {
|
|
636
666
|
type: 'object',
|
|
637
667
|
properties: {
|
|
638
668
|
planId: { type: 'string', description: 'Plan item ID.' },
|
|
639
|
-
|
|
669
|
+
data: {
|
|
670
|
+
type: 'object',
|
|
671
|
+
description: 'Canonical step payload: { text, title?, assignee? }. title is accepted as an alias for text.',
|
|
672
|
+
properties: {
|
|
673
|
+
text: { type: 'string', description: 'Step description.' },
|
|
674
|
+
title: { type: 'string', description: 'Alias for text.' },
|
|
675
|
+
assignee: { type: 'string', description: 'Optional assignee.' },
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
text: { type: 'string', description: 'Legacy top-level step description; prefer data.text.' },
|
|
640
679
|
agent: { type: 'string', description: 'Agent name.' },
|
|
641
680
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
642
|
-
assignee: { type: 'string', description: '
|
|
681
|
+
assignee: { type: 'string', description: 'Legacy top-level optional assignee; prefer data.assignee.' },
|
|
682
|
+
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
683
|
},
|
|
644
|
-
required: ['planId'
|
|
684
|
+
required: ['planId'],
|
|
645
685
|
},
|
|
646
686
|
},
|
|
647
687
|
{
|
|
648
688
|
name: 'bclaw_complete_step',
|
|
649
|
-
description: 'Mark a plan sub-step as done. Requires contributor trust level or above.',
|
|
689
|
+
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
690
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
651
691
|
inputSchema: {
|
|
652
692
|
type: 'object',
|
|
@@ -655,13 +695,14 @@ const MCP_WRITE_TOOLS = [
|
|
|
655
695
|
stepId: { type: 'string', description: 'Step ID to complete.' },
|
|
656
696
|
agent: { type: 'string', description: 'Agent name.' },
|
|
657
697
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
698
|
+
project: { type: 'string', description: 'Optional: name of a linked project to complete the step in. Defaults to the current project.' },
|
|
658
699
|
},
|
|
659
700
|
required: ['planId', 'stepId'],
|
|
660
701
|
},
|
|
661
702
|
},
|
|
662
703
|
{
|
|
663
704
|
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.',
|
|
705
|
+
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
706
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
666
707
|
inputSchema: {
|
|
667
708
|
type: 'object',
|
|
@@ -673,13 +714,14 @@ const MCP_WRITE_TOOLS = [
|
|
|
673
714
|
assignee: { type: 'string', description: 'New assignee (empty string to unassign).' },
|
|
674
715
|
agent: { type: 'string', description: 'Agent name.' },
|
|
675
716
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
717
|
+
project: { type: 'string', description: 'Optional: name of a linked project to update the step in. Defaults to the current project.' },
|
|
676
718
|
},
|
|
677
719
|
required: ['planId', 'stepId'],
|
|
678
720
|
},
|
|
679
721
|
},
|
|
680
722
|
{
|
|
681
723
|
name: 'bclaw_delete_step',
|
|
682
|
-
description: 'Remove a sub-step from a plan. Requires contributor trust level or above.',
|
|
724
|
+
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
725
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'prompt' },
|
|
684
726
|
inputSchema: {
|
|
685
727
|
type: 'object',
|
|
@@ -688,6 +730,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
688
730
|
stepId: { type: 'string', description: 'Step ID to delete.' },
|
|
689
731
|
agent: { type: 'string', description: 'Agent name.' },
|
|
690
732
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
733
|
+
project: { type: 'string', description: 'Optional: name of a linked project to delete the step from. Defaults to the current project.' },
|
|
691
734
|
},
|
|
692
735
|
required: ['planId', 'stepId'],
|
|
693
736
|
},
|
|
@@ -877,6 +920,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
877
920
|
task: { type: 'string', description: 'Optional task description (used as claim description when creating a claim).' },
|
|
878
921
|
messageId: { type: 'string', description: 'Optional message/thread ID for traceability.' },
|
|
879
922
|
contextTarget: { type: 'string', description: 'Optional path passed to bclaw_get_context to filter memory.' },
|
|
923
|
+
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
924
|
agent: { type: 'string', description: 'Agent name.' },
|
|
881
925
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
882
926
|
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 +930,13 @@ const MCP_WRITE_TOOLS = [
|
|
|
886
930
|
},
|
|
887
931
|
{
|
|
888
932
|
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
|
|
933
|
+
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
934
|
annotations: { tier: 'facade', category: 'coordination', headlessApproval: 'auto' },
|
|
891
935
|
inputSchema: {
|
|
892
936
|
type: 'object',
|
|
893
937
|
properties: {
|
|
894
938
|
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.' },
|
|
939
|
+
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
940
|
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
941
|
targetAgents: { type: 'array', items: { type: 'string' }, description: 'Agent names to target. If omitted, all spawnable agents are used.' },
|
|
898
942
|
constraints: { type: 'object', description: 'Optional structured constraints passed alongside the brief (e.g. deadline, reviewCriteria).' },
|
|
@@ -924,7 +968,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
924
968
|
intent: {
|
|
925
969
|
type: 'string',
|
|
926
970
|
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.',
|
|
971
|
+
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
972
|
},
|
|
929
973
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
930
974
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -935,7 +979,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
935
979
|
linked: { type: 'object', description: 'Optional top-level plan/sequence refs (open).' },
|
|
936
980
|
stop_condition: { type: 'object', description: 'Optional stop_condition override (open). Composite any/all supported.' },
|
|
937
981
|
mode: { type: 'string', enum: ['asymmetric', 'symmetric'], description: 'Review mode selector for open (review kind only).' },
|
|
938
|
-
status: { type: 'string', description: '
|
|
982
|
+
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
983
|
include_events: { type: 'boolean', description: 'get: include the event journal in the response.' },
|
|
940
984
|
limit: { type: 'number', description: 'list: max loops returned.' },
|
|
941
985
|
offset: { type: 'number', description: 'list: pagination offset.' },
|
|
@@ -952,6 +996,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
952
996
|
reason: { type: 'string', description: 'advance / pause / close: optional reason string.' },
|
|
953
997
|
expected_version: { type: 'number', description: 'Accepted for RFC compatibility on mutating intents, but not enforced until lock/CAS wiring lands.' },
|
|
954
998
|
client_request_id: { type: 'string', description: 'Accepted for RFC compatibility on mutating intents, but not enforced until lock/idempotency wiring lands.' },
|
|
999
|
+
project: { type: 'string', description: 'Optional linked project name/path. Routes loop reads and mutations to that project. Defaults to the current cwd.' },
|
|
955
1000
|
agent: { type: 'string', description: 'Caller agent name.' },
|
|
956
1001
|
agentId: { type: 'string', description: 'Caller registered agent id (enforced for slot-bound auth in complete_turn).' },
|
|
957
1002
|
},
|
|
@@ -1038,13 +1083,13 @@ const MCP_WRITE_TOOLS = [
|
|
|
1038
1083
|
// Promoted to `standard` tier at the v1.0 cut.
|
|
1039
1084
|
{
|
|
1040
1085
|
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.',
|
|
1086
|
+
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
1087
|
annotations: { tier: 'standard', category: 'memory', headlessApproval: 'auto' },
|
|
1043
1088
|
inputSchema: {
|
|
1044
1089
|
type: 'object',
|
|
1045
1090
|
properties: {
|
|
1046
1091
|
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).' },
|
|
1092
|
+
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
1093
|
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
1094
|
},
|
|
1050
1095
|
required: ['entity'],
|
|
@@ -2377,6 +2422,102 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2377
2422
|
}
|
|
2378
2423
|
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
2424
|
}
|
|
2425
|
+
if (name === 'bclaw_init_project') {
|
|
2426
|
+
const rawPath = typeof args.path === 'string' ? args.path.trim() : '';
|
|
2427
|
+
if (!rawPath) {
|
|
2428
|
+
return { response: createToolErrorResponse('validation_error', 'path is required') };
|
|
2429
|
+
}
|
|
2430
|
+
const force = args.force === true;
|
|
2431
|
+
const projectModeArg = typeof args.project_mode === 'string' ? args.project_mode : undefined;
|
|
2432
|
+
const linkAs = typeof args.link_as === 'string' && args.link_as.trim().length > 0
|
|
2433
|
+
? args.link_as.trim()
|
|
2434
|
+
: undefined;
|
|
2435
|
+
const resolvedPath = path.isAbsolute(rawPath) ? rawPath : path.resolve(cwd, rawPath);
|
|
2436
|
+
let wasAlreadyInitialized = false;
|
|
2437
|
+
if (memoryExists(resolvedPath) && !force) {
|
|
2438
|
+
wasAlreadyInitialized = true;
|
|
2439
|
+
}
|
|
2440
|
+
else {
|
|
2441
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
2442
|
+
try {
|
|
2443
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
2444
|
+
}
|
|
2445
|
+
catch (err) {
|
|
2446
|
+
return {
|
|
2447
|
+
response: createToolErrorResponse('init_project_failed', `Failed to create target directory '${resolvedPath}': ${err instanceof Error ? err.message : String(err)}`),
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
try {
|
|
2452
|
+
const { runInit } = await import('./init.js');
|
|
2453
|
+
await runInit({
|
|
2454
|
+
yes: true,
|
|
2455
|
+
cwd: resolvedPath,
|
|
2456
|
+
force,
|
|
2457
|
+
...(projectModeArg ? { projectMode: projectModeArg } : {}),
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
catch (err) {
|
|
2461
|
+
return {
|
|
2462
|
+
response: createToolErrorResponse('init_project_failed', `runInit failed for '${resolvedPath}': ${err instanceof Error ? err.message : String(err)}`),
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
let projectName;
|
|
2467
|
+
try {
|
|
2468
|
+
projectName = loadConfig(resolvedPath).project_name;
|
|
2469
|
+
}
|
|
2470
|
+
catch {
|
|
2471
|
+
projectName = path.basename(resolvedPath);
|
|
2472
|
+
}
|
|
2473
|
+
let linkName;
|
|
2474
|
+
try {
|
|
2475
|
+
const { addCrossProjectLink } = await import('../core/cross-project.js');
|
|
2476
|
+
const link = addCrossProjectLink({
|
|
2477
|
+
path: resolvedPath,
|
|
2478
|
+
name: linkAs ?? projectName,
|
|
2479
|
+
cwd,
|
|
2480
|
+
force,
|
|
2481
|
+
});
|
|
2482
|
+
linkName = link.name ?? path.basename(resolvedPath);
|
|
2483
|
+
}
|
|
2484
|
+
catch (err) {
|
|
2485
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2486
|
+
// Treat a duplicate link as idempotent success when the caller did
|
|
2487
|
+
// not request --force; the project itself is initialised correctly
|
|
2488
|
+
// and the existing link already points at it.
|
|
2489
|
+
if (/already exists/i.test(message) && !force) {
|
|
2490
|
+
try {
|
|
2491
|
+
const { resolveCrossProjectLinks } = await import('../core/cross-project.js');
|
|
2492
|
+
const existing = resolveCrossProjectLinks(cwd).find((l) => l.absolutePath === resolvedPath || l.path === rawPath);
|
|
2493
|
+
linkName = existing?.name ?? linkAs ?? projectName;
|
|
2494
|
+
}
|
|
2495
|
+
catch {
|
|
2496
|
+
linkName = linkAs ?? projectName;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
else {
|
|
2500
|
+
return {
|
|
2501
|
+
response: createToolErrorResponse('init_project_failed', `Failed to register cross_project_link: ${message}`),
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
const summary = wasAlreadyInitialized
|
|
2506
|
+
? `✔ ${resolvedPath} already initialised; linked as '${linkName}'.`
|
|
2507
|
+
: `✔ Initialised brainclaw at ${resolvedPath} and linked as '${linkName}'.`;
|
|
2508
|
+
return {
|
|
2509
|
+
response: toolResponse({
|
|
2510
|
+
content: [{ type: 'text', text: summary }],
|
|
2511
|
+
structuredContent: {
|
|
2512
|
+
status: 'ok',
|
|
2513
|
+
project_name: projectName,
|
|
2514
|
+
path: resolvedPath,
|
|
2515
|
+
link_id: linkName,
|
|
2516
|
+
was_already_initialized: wasAlreadyInitialized,
|
|
2517
|
+
},
|
|
2518
|
+
}),
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2380
2521
|
if (name === 'bclaw_write_note') {
|
|
2381
2522
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
|
|
2382
2523
|
if (resolved.error) {
|
|
@@ -2887,7 +3028,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
2887
3028
|
if (resolved.error && resolved.error.kind !== 'identity_error') {
|
|
2888
3029
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
2889
3030
|
}
|
|
2890
|
-
const result = startSession({
|
|
3031
|
+
const result = await startSession({
|
|
2891
3032
|
agent: resolved.identity?.agent_name ?? (typeof args.agent === 'string' ? args.agent : undefined),
|
|
2892
3033
|
agentId: resolved.identity?.agent_id ?? (typeof args.agentId === 'string' ? args.agentId : undefined),
|
|
2893
3034
|
context: args.context,
|
|
@@ -3030,7 +3171,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3030
3171
|
if (resolved.error) {
|
|
3031
3172
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
3032
3173
|
}
|
|
3033
|
-
const result = endSession({
|
|
3174
|
+
const result = await endSession({
|
|
3034
3175
|
session: args.session,
|
|
3035
3176
|
agent: resolved.identity?.agent_name,
|
|
3036
3177
|
agentId: resolved.identity?.agent_id,
|
|
@@ -3575,13 +3716,22 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3575
3716
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
3576
3717
|
}
|
|
3577
3718
|
const stepPlanId = String(args.planId ?? '').trim();
|
|
3578
|
-
const
|
|
3719
|
+
const stepData = args.data && typeof args.data === 'object' && !Array.isArray(args.data)
|
|
3720
|
+
? args.data
|
|
3721
|
+
: {};
|
|
3722
|
+
if ((args.text !== undefined || args.assignee !== undefined) && Object.keys(stepData).length > 0) {
|
|
3723
|
+
console.warn('[brainclaw:warn] bclaw_add_step received legacy top-level fields alongside data.*; using data.* values');
|
|
3724
|
+
}
|
|
3725
|
+
const stepTextRaw = stepData.text ?? stepData.title ?? args.text;
|
|
3726
|
+
const stepText = typeof stepTextRaw === 'string' ? stepTextRaw.trim() : '';
|
|
3727
|
+
const stepAssignee = (stepData.assignee ?? args.assignee);
|
|
3579
3728
|
if (!stepPlanId)
|
|
3580
3729
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
3581
3730
|
if (!stepText)
|
|
3582
|
-
return { response: createToolErrorResponse('validation_error', 'Missing required argument: text') };
|
|
3731
|
+
return { response: createToolErrorResponse('validation_error', 'Missing required argument: data.text') };
|
|
3732
|
+
const stepTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3583
3733
|
try {
|
|
3584
|
-
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee:
|
|
3734
|
+
const result = addStepOp({ planId: stepPlanId, text: stepText, assignee: stepAssignee }, stepTargetCwd);
|
|
3585
3735
|
return {
|
|
3586
3736
|
response: toolResponse({
|
|
3587
3737
|
content: [{ type: 'text', text: `✔ Step added: [${result.stepId}] ${stepText} (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
@@ -3614,8 +3764,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3614
3764
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
3615
3765
|
if (!csStepId)
|
|
3616
3766
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
|
|
3767
|
+
const csTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3617
3768
|
try {
|
|
3618
|
-
const result = completeStepOp({ planId: csPlanId, stepId: csStepId },
|
|
3769
|
+
const result = completeStepOp({ planId: csPlanId, stepId: csStepId }, csTargetCwd);
|
|
3619
3770
|
return {
|
|
3620
3771
|
response: toolResponse({
|
|
3621
3772
|
content: [{ type: 'text', text: `✔ Step completed: [${result.stepId}] (${result.doneSteps}/${result.totalSteps} done)` }],
|
|
@@ -3653,6 +3804,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3653
3804
|
if (args.status && !validStatuses.includes(String(args.status))) {
|
|
3654
3805
|
return { response: createToolErrorResponse('validation_error', `Invalid status: ${args.status}. Valid: ${validStatuses.join(', ')}`) };
|
|
3655
3806
|
}
|
|
3807
|
+
const usTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3656
3808
|
try {
|
|
3657
3809
|
const result = updateStepOp({
|
|
3658
3810
|
planId: usPlanId,
|
|
@@ -3660,7 +3812,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3660
3812
|
status: args.status,
|
|
3661
3813
|
text: args.text,
|
|
3662
3814
|
assignee: args.assignee,
|
|
3663
|
-
},
|
|
3815
|
+
}, usTargetCwd);
|
|
3664
3816
|
const changes = [];
|
|
3665
3817
|
if (args.status)
|
|
3666
3818
|
changes.push(`status=${args.status}`);
|
|
@@ -3701,8 +3853,9 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3701
3853
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: planId') };
|
|
3702
3854
|
if (!dsStepId)
|
|
3703
3855
|
return { response: createToolErrorResponse('validation_error', 'Missing required argument: stepId') };
|
|
3856
|
+
const dsTargetCwd = resolveProjectCwd(args.project, cwd);
|
|
3704
3857
|
try {
|
|
3705
|
-
const result = deleteStepOp({ planId: dsPlanId, stepId: dsStepId },
|
|
3858
|
+
const result = deleteStepOp({ planId: dsPlanId, stepId: dsStepId }, dsTargetCwd);
|
|
3706
3859
|
return {
|
|
3707
3860
|
response: toolResponse({
|
|
3708
3861
|
content: [{ type: 'text', text: `✔ Step deleted: [${result.stepId}] (${result.doneSteps}/${result.totalSteps} remaining)` }],
|
|
@@ -4075,16 +4228,17 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4075
4228
|
return { response: createToolErrorResponse('validation_error', parseResult.error.message) };
|
|
4076
4229
|
}
|
|
4077
4230
|
const workReq = parseResult.data;
|
|
4231
|
+
const targetCwd = resolveProjectCwd(workReq.project, cwd);
|
|
4078
4232
|
const useCompact = workReq.compact !== false; // default true
|
|
4079
4233
|
const warnings = [];
|
|
4080
4234
|
// Step 1: implicit session start (handles auto-registration internally)
|
|
4081
4235
|
let sessionResult;
|
|
4082
4236
|
try {
|
|
4083
|
-
sessionResult = startSession({
|
|
4237
|
+
sessionResult = await startSession({
|
|
4084
4238
|
agent: typeof args.agent === 'string' ? args.agent : undefined,
|
|
4085
4239
|
agentId: typeof args.agentId === 'string' ? args.agentId : undefined,
|
|
4086
4240
|
context: workReq.contextTarget,
|
|
4087
|
-
cwd,
|
|
4241
|
+
cwd: targetCwd,
|
|
4088
4242
|
});
|
|
4089
4243
|
}
|
|
4090
4244
|
catch (sessionErr) {
|
|
@@ -4102,14 +4256,14 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4102
4256
|
try {
|
|
4103
4257
|
let sinceSession;
|
|
4104
4258
|
if (workReq.intent === 'resume') {
|
|
4105
|
-
const previousSession = loadAllSessions(
|
|
4259
|
+
const previousSession = loadAllSessions(targetCwd)
|
|
4106
4260
|
.find((s) => s.agent === sessionResult.agent && s.session_id !== sessionResult.session_id);
|
|
4107
4261
|
sinceSession = previousSession?.session_id;
|
|
4108
4262
|
}
|
|
4109
4263
|
contextResult = buildContext({
|
|
4110
4264
|
target: workReq.contextTarget ?? workReq.scope,
|
|
4111
4265
|
agent: sessionResult.agent,
|
|
4112
|
-
cwd,
|
|
4266
|
+
cwd: targetCwd,
|
|
4113
4267
|
sinceSession,
|
|
4114
4268
|
});
|
|
4115
4269
|
}
|
|
@@ -4118,7 +4272,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4118
4272
|
let claimId;
|
|
4119
4273
|
let claimStatus = 'none';
|
|
4120
4274
|
if (workReq.intent === 'execute' && workReq.scope) {
|
|
4121
|
-
const existingClaims = listClaims(
|
|
4275
|
+
const existingClaims = listClaims(targetCwd).filter((c) => c.status === 'active' && c.agent === sessionResult.agent && c.scope === workReq.scope);
|
|
4122
4276
|
if (existingClaims.length > 0) {
|
|
4123
4277
|
claimId = existingClaims[0].id;
|
|
4124
4278
|
claimStatus = 'existing';
|
|
@@ -4139,11 +4293,11 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4139
4293
|
status: 'active',
|
|
4140
4294
|
plan_id: workReq.planId,
|
|
4141
4295
|
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 },
|
|
4296
|
+
}, targetCwd);
|
|
4297
|
+
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
4298
|
claimStatus = 'created';
|
|
4145
4299
|
// Policy check post-claim
|
|
4146
|
-
const policyResult = checkPolicy({ scope: workReq.scope, agent: sessionResult.agent, agentId: sessionResult.agent_id, cwd });
|
|
4300
|
+
const policyResult = checkPolicy({ scope: workReq.scope, agent: sessionResult.agent, agentId: sessionResult.agent_id, cwd: targetCwd });
|
|
4147
4301
|
for (const w of policyResult.warnings.filter((pw) => pw.kind !== 'no_claim')) {
|
|
4148
4302
|
const idLabel = w.id ? ` (${w.id})` : '';
|
|
4149
4303
|
warnings.push(`[${w.kind}]${idLabel} ${w.message}`);
|
|
@@ -4185,6 +4339,34 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4185
4339
|
_full_context_hint: 'Use bclaw_context(kind="memory") for the full payload.',
|
|
4186
4340
|
};
|
|
4187
4341
|
}
|
|
4342
|
+
// pln#513 step 1 — bootstrap hint. When the project lacks a usable
|
|
4343
|
+
// PROJECT.md (absent or 0 bytes), surface a hint so the agent knows
|
|
4344
|
+
// the canonical entry-point. Cheap probe — one fs.statSync, no
|
|
4345
|
+
// gating flag. The literal next_action mirrors the documented
|
|
4346
|
+
// canonical-grammar call so callers can suggest it verbatim.
|
|
4347
|
+
let bootstrapRecommended;
|
|
4348
|
+
let nextAction;
|
|
4349
|
+
try {
|
|
4350
|
+
const fsMod = await import('node:fs');
|
|
4351
|
+
const pathMod = await import('node:path');
|
|
4352
|
+
const projectMdPath = pathMod.join(targetCwd, 'PROJECT.md');
|
|
4353
|
+
let needsBootstrap = false;
|
|
4354
|
+
try {
|
|
4355
|
+
const stat = fsMod.statSync(projectMdPath);
|
|
4356
|
+
if (!stat.isFile() || stat.size === 0)
|
|
4357
|
+
needsBootstrap = true;
|
|
4358
|
+
}
|
|
4359
|
+
catch {
|
|
4360
|
+
needsBootstrap = true; // ENOENT → absent
|
|
4361
|
+
}
|
|
4362
|
+
bootstrapRecommended = needsBootstrap;
|
|
4363
|
+
if (needsBootstrap) {
|
|
4364
|
+
nextAction = "bclaw_coordinate(intent='ideate', preset='bootstrap')";
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
catch {
|
|
4368
|
+
// Best-effort: never block bclaw_work on the probe.
|
|
4369
|
+
}
|
|
4188
4370
|
const facadeResponse = {
|
|
4189
4371
|
status: 'ok',
|
|
4190
4372
|
intent: workReq.intent,
|
|
@@ -4195,12 +4377,17 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4195
4377
|
session_id: sessionResult.session_id,
|
|
4196
4378
|
warnings,
|
|
4197
4379
|
duration_ms: Date.now() - startMs,
|
|
4380
|
+
bootstrap_recommended: bootstrapRecommended,
|
|
4381
|
+
next_action: nextAction,
|
|
4198
4382
|
};
|
|
4199
4383
|
const summaryParts = [`✔ bclaw_work [${workReq.intent}] session=${sessionResult.session_id}`];
|
|
4200
4384
|
if (claimId)
|
|
4201
4385
|
summaryParts.push(`claim=${claimId} (${claimStatus})`);
|
|
4202
4386
|
if (useCompact)
|
|
4203
4387
|
summaryParts.push('mode=compact (use bclaw_context for full payload)');
|
|
4388
|
+
if (bootstrapRecommended) {
|
|
4389
|
+
summaryParts.push(`💡 PROJECT.md missing — call ${nextAction} to open a bootstrap loop`);
|
|
4390
|
+
}
|
|
4204
4391
|
if (warnings.length > 0)
|
|
4205
4392
|
summaryParts.push(warnings.map((w) => `⚠ ${w}`).join('\n'));
|
|
4206
4393
|
return {
|
|
@@ -4218,15 +4405,89 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4218
4405
|
return { response: createToolErrorResponse('validation_error', parseResult.error.message) };
|
|
4219
4406
|
}
|
|
4220
4407
|
const req = parseResult.data;
|
|
4408
|
+
// pln#511 step 2 — preset selector validation. Presets are kind-
|
|
4409
|
+
// specific in v1: only intent='ideate' carries them. Unknown names
|
|
4410
|
+
// are rejected up-front against the registry so the handler never
|
|
4411
|
+
// silently falls back to the kind-default. The bootstrap preset
|
|
4412
|
+
// also enforces a dispatch constraint (can_753a083a): the champion
|
|
4413
|
+
// must be a human-connected agent, never a sandboxed worker —
|
|
4414
|
+
// checked below once the senderAgent is resolved.
|
|
4415
|
+
if (req.preset !== undefined) {
|
|
4416
|
+
if (req.intent !== 'ideate') {
|
|
4417
|
+
return {
|
|
4418
|
+
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'.`),
|
|
4419
|
+
};
|
|
4420
|
+
}
|
|
4421
|
+
const { PRESETS: PRESETS_REGISTRY } = await import('../core/loops/presets/index.js');
|
|
4422
|
+
if (!(req.preset in PRESETS_REGISTRY)) {
|
|
4423
|
+
const validNames = Object.keys(PRESETS_REGISTRY).join(', ') || '(none)';
|
|
4424
|
+
return {
|
|
4425
|
+
response: createToolErrorResponse('unknown_preset', `Unknown preset '${req.preset}'. Valid preset names: ${validNames}.`),
|
|
4426
|
+
};
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
// can_30c295b4 — pre-flight uncommitted-changes check.
|
|
4430
|
+
// Dispatches that spawn a worker (review/assign/consult/ideate) clone
|
|
4431
|
+
// the source repo at HEAD into a worktree. Any uncommitted edits in
|
|
4432
|
+
// the source cwd are invisible to the worker, so the worker reviews
|
|
4433
|
+
// stale code without any error signal. Refuse the dispatch by default
|
|
4434
|
+
// when the source cwd is a git repo with a dirty working tree.
|
|
4435
|
+
// Override via allow_dirty=true when the caller knows the dispatched
|
|
4436
|
+
// work doesn't depend on the modified files (e.g. tests, docs-only
|
|
4437
|
+
// worker tasks). Has no effect when cwd is not a git repo.
|
|
4438
|
+
if (!req.allow_dirty && (req.intent === 'review' || req.intent === 'assign' || req.intent === 'consult' || req.intent === 'ideate')) {
|
|
4439
|
+
try {
|
|
4440
|
+
const { spawnSync } = await import('node:child_process');
|
|
4441
|
+
const probe = spawnSync('git', ['-C', cwd, 'status', '--porcelain'], { encoding: 'utf8', timeout: 5000 });
|
|
4442
|
+
if (probe.status === 0 && typeof probe.stdout === 'string' && probe.stdout.trim().length > 0) {
|
|
4443
|
+
const fileCount = probe.stdout.trim().split('\n').length;
|
|
4444
|
+
return {
|
|
4445
|
+
response: createToolErrorResponse('dirty_working_tree', `Refusing to dispatch: source working tree has ${fileCount} uncommitted file(s). The spawned worker will not see these changes (worktrees branch from HEAD). Commit or stash before dispatching, or pass allow_dirty=true to override. Source cwd: ${cwd}`),
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
// probe.status !== 0 → not a git repo (or git unavailable) → skip the check silently
|
|
4449
|
+
}
|
|
4450
|
+
catch {
|
|
4451
|
+
// best-effort: never block dispatch on a pre-flight check failure unrelated to the actual dirty state
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4221
4454
|
const warnings = [];
|
|
4222
4455
|
const artifacts = [];
|
|
4223
4456
|
const side_effects = [];
|
|
4457
|
+
// can_5e62334e — codex sandboxed dispatches cannot commit in worktrees
|
|
4458
|
+
// because `.git` is a file pointer to the parent repo's
|
|
4459
|
+
// .git/worktrees/<wt>/ directory, which lives OUTSIDE the worktree's
|
|
4460
|
+
// writable root that `--sandbox workspace-write` permits. Any
|
|
4461
|
+
// codex worker that runs `git commit` will fail with `index.lock:
|
|
4462
|
+
// Permission denied`. Warn callers up-front so briefs don't request
|
|
4463
|
+
// per-bug commits; the coordinator must harvest the worktree
|
|
4464
|
+
// diff via `git diff` and commit from a non-sandboxed cwd.
|
|
4465
|
+
if (Array.isArray(req.targetAgents) && req.targetAgents.includes('codex')) {
|
|
4466
|
+
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.');
|
|
4467
|
+
}
|
|
4224
4468
|
const senderAgent = typeof args.agent === 'string' && args.agent.trim()
|
|
4225
4469
|
? args.agent.trim()
|
|
4226
4470
|
: 'bclaw_coordinate';
|
|
4227
4471
|
const senderAgentId = typeof args.agentId === 'string' && args.agentId.trim()
|
|
4228
4472
|
? args.agentId.trim()
|
|
4229
4473
|
: undefined;
|
|
4474
|
+
// pln#511 step 2 — bootstrap preset dispatch constraint (can_753a083a).
|
|
4475
|
+
// The bootstrap loop's champion must be a human-connected agent: it
|
|
4476
|
+
// asks the operator clarifying questions and writes PROJECT.md. A
|
|
4477
|
+
// sandboxed worker (codex / github-copilot) cannot reach the human,
|
|
4478
|
+
// so the loop would stall in `clarify`. Enforce by requiring
|
|
4479
|
+
// targetAgents to be empty (= single-agent / self-champion mode)
|
|
4480
|
+
// or to contain only the caller. Other presets are unrestricted.
|
|
4481
|
+
if (req.preset === 'bootstrap') {
|
|
4482
|
+
const targets = req.targetAgents ?? [];
|
|
4483
|
+
const onlyCaller = targets.length === 0
|
|
4484
|
+
|| (targets.length === 1 && targets[0] === senderAgent);
|
|
4485
|
+
if (!onlyCaller) {
|
|
4486
|
+
return {
|
|
4487
|
+
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)}].`),
|
|
4488
|
+
};
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4230
4491
|
const commandHints = [];
|
|
4231
4492
|
const preparedInvokes = [];
|
|
4232
4493
|
// pln#359 phase 1b — cross-project routing. When `project` is set, all
|
|
@@ -5043,186 +5304,318 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5043
5304
|
: messages.map((m, i) => `[${i + 1}] ${m.from} → ${m.to}: ${m.text}`).join('\n');
|
|
5044
5305
|
result = { thread_id: threadId, message_count: messages.length, summary };
|
|
5045
5306
|
}
|
|
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
|
-
|
|
5307
|
+
else if (req.intent === 'ideate')
|
|
5308
|
+
ideate: {
|
|
5309
|
+
// pln#492 phase 2.c (open + proposal) + 2.d.2 (multi-agent dispatch).
|
|
5310
|
+
// Single-agent mode (no targetAgents): open the loop with the task
|
|
5311
|
+
// as a proposal seed and stop there — the champion drives manually
|
|
5312
|
+
// via bclaw_loop intent='turn' / 'advance'. Multi-agent mode
|
|
5313
|
+
// (targetAgents passed): also advance to critique and dispatch a
|
|
5314
|
+
// turn to each critic slot with a context-filtered, BM25-ranked,
|
|
5315
|
+
// size-capped brief assembled by buildIdeationBrief.
|
|
5316
|
+
//
|
|
5317
|
+
// pln#511 step 2 — when `req.preset` is set, the loop opens with
|
|
5318
|
+
// the preset's phases / stop_condition / protocol instead of the
|
|
5319
|
+
// kind-default ideation chain. Bootstrap preset enforces single-
|
|
5320
|
+
// agent / self-champion mode (validated above), so the multi-
|
|
5321
|
+
// agent dispatch branch never runs for it.
|
|
5322
|
+
//
|
|
5323
|
+
// pln#513 step 2 — labelled block (ideate:) so the bootstrap
|
|
5324
|
+
// join-or-lock path can break out early after assigning result.
|
|
5325
|
+
const loopsModuleRef = await import('../core/loops/index.js');
|
|
5326
|
+
const { openLoop, add_artifact, advance, turn, getLoop, buildIdeationBrief } = loopsModuleRef;
|
|
5327
|
+
const presetSelected = req.preset
|
|
5328
|
+
? (await import('../core/loops/presets/index.js')).PRESETS[req.preset]
|
|
5329
|
+
: undefined;
|
|
5330
|
+
// pln#513 step 2 — bootstrap join-or-lock. The bootstrap preset is
|
|
5331
|
+
// project-singleton: two concurrent callers must converge on the same
|
|
5332
|
+
// loop rather than open duplicates. Strategy:
|
|
5333
|
+
// 1. Find existing bootstrap loop in {open, paused} → join.
|
|
5334
|
+
// 2. Else check for an active coordination claim (someone is
|
|
5335
|
+
// mid-open). Re-find once; surface bootstrap_coordination_in_progress
|
|
5336
|
+
// if still nothing.
|
|
5337
|
+
// 3. Else acquire the lock, fall through to the normal open path,
|
|
5338
|
+
// release the lock on the way out (success or fail).
|
|
5339
|
+
// The lock is opportunistic, not blocking — a fast retry-in-place
|
|
5340
|
+
// not a wait-on-mutex. Keeps the verb short and predictable.
|
|
5341
|
+
// pln#518 step 1 — bootstrap join-or-lock now delegates to the shared
|
|
5342
|
+
// acquireBootstrapLoop helper (src/core/loops/bootstrap-acquire.ts).
|
|
5343
|
+
// Both the CLI and this MCP handler converge on the same singleton
|
|
5344
|
+
// acquire path, eliminating the race where two concurrent callers both
|
|
5345
|
+
// passed the local scan and called openLoop directly.
|
|
5346
|
+
const senderIdentity = ((senderAgentId ? findAgentIdentityById(senderAgentId, dispatchCwd) : undefined)
|
|
5347
|
+
?? findAgentIdentityByName(senderAgent, dispatchCwd)
|
|
5348
|
+
?? ensureAgentRegisteredForDispatch(senderAgent, dispatchCwd));
|
|
5349
|
+
const authorAgentId = senderIdentity?.agent_id ?? senderAgentId;
|
|
5350
|
+
const creatorActor = authorAgentId ?? senderAgent;
|
|
5351
|
+
let bootstrapOpenedLoop;
|
|
5352
|
+
if (req.preset === 'bootstrap') {
|
|
5353
|
+
const { acquireBootstrapLoop, BootstrapCoordinationInProgressError: BcipError } = await import('../core/loops/bootstrap-acquire.js');
|
|
5354
|
+
let acqResult;
|
|
5355
|
+
try {
|
|
5356
|
+
acqResult = acquireBootstrapLoop({
|
|
5357
|
+
actor: senderAgent,
|
|
5358
|
+
agent_id: authorAgentId,
|
|
5359
|
+
created_by: creatorActor,
|
|
5360
|
+
title: req.task.slice(0, 120),
|
|
5361
|
+
goal: req.scope,
|
|
5362
|
+
model: currentModel,
|
|
5363
|
+
}, dispatchCwd);
|
|
5364
|
+
}
|
|
5365
|
+
catch (err) {
|
|
5366
|
+
if (err instanceof BcipError) {
|
|
5367
|
+
return {
|
|
5368
|
+
response: createToolErrorResponse('bootstrap_coordination_in_progress', err.message),
|
|
5369
|
+
};
|
|
5370
|
+
}
|
|
5371
|
+
throw err;
|
|
5372
|
+
}
|
|
5373
|
+
warnings.push(...acqResult.warnings);
|
|
5374
|
+
if (acqResult.action === 'joined') {
|
|
5375
|
+
const jLoop = acqResult.loop;
|
|
5376
|
+
artifacts.push({ type: 'loop', id: jLoop.id });
|
|
5377
|
+
result = {
|
|
5378
|
+
loop_id: jLoop.id,
|
|
5379
|
+
joined_existing: true,
|
|
5380
|
+
current_phase: jLoop.current_phase,
|
|
5381
|
+
status: jLoop.status,
|
|
5382
|
+
mode: 'single_agent',
|
|
5383
|
+
preset: req.preset,
|
|
5384
|
+
};
|
|
5385
|
+
// Skip the rest of the ideate flow — we joined an existing loop.
|
|
5386
|
+
break ideate;
|
|
5387
|
+
}
|
|
5388
|
+
// action === 'opened': helper already called openLoop + released lock.
|
|
5389
|
+
bootstrapOpenedLoop = acqResult.loop;
|
|
5077
5390
|
}
|
|
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,
|
|
5391
|
+
// pln#511 step 2 — bootstrap preset always runs in single-agent
|
|
5392
|
+
// mode: the champion drives the whole loop. Even when the caller
|
|
5393
|
+
// passes targetAgents=[caller] (validated as the only legal non-
|
|
5394
|
+
// empty form), we don't add critic slots and we don't take the
|
|
5395
|
+
// multi-agent dispatch branch. Treating that idiom as "single
|
|
5396
|
+
// agent / self-champion" matches what the constraint check
|
|
5397
|
+
// already enforced upstream.
|
|
5398
|
+
const explicitTargets = Boolean(req.targetAgents
|
|
5399
|
+
&& req.targetAgents.length > 0
|
|
5400
|
+
&& req.preset !== 'bootstrap');
|
|
5401
|
+
const slots = [
|
|
5402
|
+
{
|
|
5403
|
+
role: 'champion',
|
|
5404
|
+
agent: senderAgent,
|
|
5405
|
+
...(authorAgentId ? { agent_id: authorAgentId } : {}),
|
|
5101
5406
|
},
|
|
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,
|
|
5407
|
+
];
|
|
5408
|
+
if (explicitTargets) {
|
|
5409
|
+
for (const agent of req.targetAgents) {
|
|
5410
|
+
const criticIdentity = findAgentIdentityByName(agent, dispatchCwd) ?? ensureAgentRegisteredForDispatch(agent, dispatchCwd);
|
|
5411
|
+
slots.push({
|
|
5412
|
+
role: 'critic',
|
|
5413
|
+
agent,
|
|
5414
|
+
...(criticIdentity?.agent_id ? { agent_id: criticIdentity.agent_id } : {}),
|
|
5173
5415
|
});
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5416
|
+
}
|
|
5417
|
+
}
|
|
5418
|
+
let loopId;
|
|
5419
|
+
let proposalArtifactId;
|
|
5420
|
+
if (bootstrapOpenedLoop) {
|
|
5421
|
+
// Bootstrap case: helper already opened the loop and released its lock.
|
|
5422
|
+
loopId = bootstrapOpenedLoop.id;
|
|
5423
|
+
artifacts.push({ type: 'loop', id: bootstrapOpenedLoop.id });
|
|
5424
|
+
side_effects.push({ action: 'create', entity: 'loop', id: bootstrapOpenedLoop.id });
|
|
5425
|
+
}
|
|
5426
|
+
else {
|
|
5427
|
+
try {
|
|
5428
|
+
const loop = openLoop({
|
|
5429
|
+
kind: 'ideation',
|
|
5430
|
+
title: req.task.slice(0, 120),
|
|
5431
|
+
goal: req.scope,
|
|
5432
|
+
created_by: creatorActor,
|
|
5433
|
+
slots,
|
|
5434
|
+
...(presetSelected
|
|
5435
|
+
? {
|
|
5436
|
+
phases: presetSelected.phases,
|
|
5437
|
+
stop_condition: presetSelected.stop_condition,
|
|
5438
|
+
protocol: presetSelected.protocol,
|
|
5439
|
+
}
|
|
5440
|
+
: {}),
|
|
5179
5441
|
}, dispatchCwd);
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5442
|
+
loopId = loop.id;
|
|
5443
|
+
artifacts.push({ type: 'loop', id: loop.id });
|
|
5444
|
+
side_effects.push({ action: 'create', entity: 'loop', id: loop.id });
|
|
5445
|
+
// pln#511 step 2 — skip the proposal-seed artifact when a preset
|
|
5446
|
+
// is in use. The kind-default ideation chain opens at phase
|
|
5447
|
+
// 'proposal' and the seed artifact lives there; presets define
|
|
5448
|
+
// their own initial phase + seeding semantics (bootstrap starts
|
|
5449
|
+
// at 'survey' and produces a signals_report). Forcing a
|
|
5450
|
+
// 'proposal'-phased artifact here would dangle on a phase the
|
|
5451
|
+
// loop doesn't contain. The task text is already captured on
|
|
5452
|
+
// the thread (title + goal).
|
|
5453
|
+
if (!presetSelected) {
|
|
5454
|
+
const proposalBody = req.task.slice(0, 4000);
|
|
5455
|
+
const updated = add_artifact({
|
|
5456
|
+
id: loop.id,
|
|
5457
|
+
actor: creatorActor,
|
|
5458
|
+
artifact: {
|
|
5459
|
+
phase: 'proposal',
|
|
5460
|
+
type: 'proposal',
|
|
5461
|
+
body: proposalBody,
|
|
5462
|
+
produced_by: creatorActor,
|
|
5463
|
+
},
|
|
5464
|
+
}, dispatchCwd);
|
|
5465
|
+
const lastArtifact = updated.artifacts[updated.artifacts.length - 1];
|
|
5466
|
+
proposalArtifactId = lastArtifact?.artifact_id;
|
|
5467
|
+
if (proposalArtifactId) {
|
|
5468
|
+
artifacts.push({ type: 'artifact', id: proposalArtifactId });
|
|
5469
|
+
side_effects.push({ action: 'create', entity: 'artifact', id: proposalArtifactId });
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
catch (err) {
|
|
5474
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5475
|
+
return {
|
|
5476
|
+
response: createToolErrorResponse('ideate_failed', `ideate intent: failed to open ideation loop — ${msg}`),
|
|
5477
|
+
};
|
|
5478
|
+
}
|
|
5479
|
+
} // end else (non-bootstrap open path)
|
|
5480
|
+
// pln#492 phase 2.d.2 — multi-agent dispatch. Skipped in single-
|
|
5481
|
+
// agent mode (the champion drives manually).
|
|
5482
|
+
//
|
|
5483
|
+
// pln#511 step 2 — initial phase comes from the actual loop's
|
|
5484
|
+
// first phase, not a hardcoded 'proposal'. Presets like bootstrap
|
|
5485
|
+
// open at 'survey'; the kind-default ideation chain still opens
|
|
5486
|
+
// at 'proposal', so this is backward compatible.
|
|
5487
|
+
let dispatchedCritics = 0;
|
|
5488
|
+
let dispatchedPhase = presetSelected ? presetSelected.phases[0].name : 'proposal';
|
|
5489
|
+
if (explicitTargets) {
|
|
5490
|
+
try {
|
|
5491
|
+
// Build a search-backed BriefMemoryProvider. Maps user-facing
|
|
5492
|
+
// memory categories the brief asks for onto src/core/search.ts
|
|
5493
|
+
// sections (BM25 there). Loop-internal categories
|
|
5494
|
+
// (critique_history / revision_history / synthesis_artifact)
|
|
5495
|
+
// are pulled by the assembler from the thread directly.
|
|
5496
|
+
const searchModule = await import('../core/search.js');
|
|
5497
|
+
const sectionByCategory = {
|
|
5498
|
+
traps: 'traps',
|
|
5499
|
+
decisions: 'decisions',
|
|
5500
|
+
constraints: 'constraints',
|
|
5501
|
+
handoffs: 'handoffs',
|
|
5502
|
+
plans: 'plans',
|
|
5503
|
+
candidates: 'candidates',
|
|
5504
|
+
};
|
|
5505
|
+
const provider = {
|
|
5506
|
+
fetch(category, query, topK) {
|
|
5507
|
+
const section = sectionByCategory[category];
|
|
5508
|
+
if (!section)
|
|
5509
|
+
return [];
|
|
5510
|
+
const results = searchModule.search({
|
|
5511
|
+
query,
|
|
5512
|
+
section,
|
|
5513
|
+
maxResults: topK,
|
|
5514
|
+
cwd: dispatchCwd,
|
|
5515
|
+
includePending: section === 'candidates',
|
|
5516
|
+
});
|
|
5517
|
+
return results.map((r) => ({
|
|
5518
|
+
id: r.id,
|
|
5519
|
+
category,
|
|
5520
|
+
text: r.text,
|
|
5521
|
+
score: r.score,
|
|
5522
|
+
}));
|
|
5194
5523
|
},
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5524
|
+
};
|
|
5525
|
+
// Advance proposal → critique. proposal has no advance_gate so
|
|
5526
|
+
// this is unconditional. After advance, the loop sits at the
|
|
5527
|
+
// critique phase ready for critic turns.
|
|
5528
|
+
advance({ id: loopId, actor: creatorActor }, dispatchCwd);
|
|
5529
|
+
const advancedLoop = getLoop(loopId, dispatchCwd);
|
|
5530
|
+
if (!advancedLoop) {
|
|
5531
|
+
throw new Error('ideate dispatch: loop disappeared after advance');
|
|
5532
|
+
}
|
|
5533
|
+
dispatchedPhase = advancedLoop.current_phase;
|
|
5534
|
+
const criticSlots = advancedLoop.slots.filter((s) => s.role === 'critic');
|
|
5535
|
+
for (const slot of criticSlots) {
|
|
5536
|
+
if (!slot.agent)
|
|
5537
|
+
continue;
|
|
5538
|
+
const briefResult = buildIdeationBrief({
|
|
5539
|
+
thread: advancedLoop,
|
|
5540
|
+
slotRole: slot.role,
|
|
5541
|
+
memoryProvider: provider,
|
|
5542
|
+
});
|
|
5543
|
+
turn({
|
|
5544
|
+
id: loopId,
|
|
5545
|
+
slot_id: slot.slot_id,
|
|
5546
|
+
actor: creatorActor,
|
|
5547
|
+
input: briefResult.text,
|
|
5548
|
+
}, dispatchCwd);
|
|
5549
|
+
const queued = queueCoordinateMessage({
|
|
5550
|
+
agent: slot.agent,
|
|
5551
|
+
text: briefResult.text,
|
|
5552
|
+
messageType: 'rfc',
|
|
5553
|
+
ref: loopId,
|
|
5554
|
+
scope: req.scope,
|
|
5555
|
+
tags: ['coordinate', 'ideate', 'loop'],
|
|
5556
|
+
payload: {
|
|
5557
|
+
intent: 'ideate',
|
|
5558
|
+
loop_id: loopId,
|
|
5559
|
+
slot_id: slot.slot_id,
|
|
5560
|
+
phase: advancedLoop.current_phase,
|
|
5561
|
+
iteration: advancedLoop.iteration_count,
|
|
5562
|
+
proposal_artifact_id: proposalArtifactId,
|
|
5563
|
+
},
|
|
5564
|
+
commandMode: 'consult',
|
|
5565
|
+
});
|
|
5566
|
+
void queued;
|
|
5567
|
+
dispatchedCritics += 1;
|
|
5568
|
+
if (briefResult.truncated) {
|
|
5569
|
+
warnings.push(`Brief for critic slot ${slot.slot_id} (${slot.agent}) truncated: ${briefResult.droppedItems} memory items dropped to fit cap`);
|
|
5570
|
+
}
|
|
5201
5571
|
}
|
|
5202
5572
|
}
|
|
5573
|
+
catch (err) {
|
|
5574
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5575
|
+
warnings.push(`ideate dispatch failed: ${msg}; loop ${loopId} stays at proposal phase`);
|
|
5576
|
+
facadeStatus = 'partial';
|
|
5577
|
+
}
|
|
5203
5578
|
}
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5579
|
+
if (!explicitTargets) {
|
|
5580
|
+
warnings.push(presetSelected
|
|
5581
|
+
? `ideate single-agent mode (preset='${req.preset}'): loop opened at phase '${dispatchedPhase}'; champion drives manually via bclaw_loop intent='turn' / 'advance'.`
|
|
5582
|
+
: "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
5583
|
}
|
|
5584
|
+
result = {
|
|
5585
|
+
loop_id: loopId,
|
|
5586
|
+
proposal_artifact_id: proposalArtifactId,
|
|
5587
|
+
selected_targets: explicitTargets ? req.targetAgents : [],
|
|
5588
|
+
mode: explicitTargets ? 'multi_agent' : 'single_agent',
|
|
5589
|
+
dispatched_critics: dispatchedCritics,
|
|
5590
|
+
current_phase: dispatchedPhase,
|
|
5591
|
+
...(presetSelected ? { preset: req.preset } : {}),
|
|
5592
|
+
};
|
|
5593
|
+
// pln#518 step 1 — bootstrap lock is now managed inside acquireBootstrapLoop;
|
|
5594
|
+
// no release needed here.
|
|
5209
5595
|
}
|
|
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
5596
|
// Extract execution_status from result if present (assign/reroute set it)
|
|
5223
5597
|
const resultExecStatus = (result && typeof result === 'object' && 'execution_status' in result)
|
|
5224
5598
|
? result.execution_status
|
|
5225
5599
|
: undefined;
|
|
5600
|
+
// pln#503 phase 3.3: when execution_status === 'delivered_and_started',
|
|
5601
|
+
// attach a self-documenting `verify_with` hint pointing at the assignment
|
|
5602
|
+
// record. Callers should not take delivered_and_started at face value —
|
|
5603
|
+
// it only attests the brief-ack sentinel was touched, not that the worker
|
|
5604
|
+
// is doing useful work. The hint tells them exactly which canonical-
|
|
5605
|
+
// grammar call to make next to verify spawn liveness.
|
|
5606
|
+
let verifyWith;
|
|
5607
|
+
if (resultExecStatus === 'delivered_and_started') {
|
|
5608
|
+
const firstAssignment = artifacts.find((a) => a.type === 'assignment');
|
|
5609
|
+
if (firstAssignment) {
|
|
5610
|
+
verifyWith = {
|
|
5611
|
+
action: 'bclaw_find',
|
|
5612
|
+
entity: 'agent_run',
|
|
5613
|
+
filter: { assignment_id: firstAssignment.id },
|
|
5614
|
+
expected_when_alive: 'agent_run with status="running" AND OS pid alive AND last_event_at within the last few minutes',
|
|
5615
|
+
see_also: 'docs/concepts/dispatch-lifecycle.md',
|
|
5616
|
+
};
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5226
5619
|
const facadeResponse = {
|
|
5227
5620
|
status: facadeStatus,
|
|
5228
5621
|
intent: req.intent,
|
|
@@ -5232,6 +5625,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5232
5625
|
warnings,
|
|
5233
5626
|
duration_ms: Date.now() - startMs,
|
|
5234
5627
|
...(resultExecStatus ? { execution_status: resultExecStatus } : {}),
|
|
5628
|
+
...(verifyWith ? { verify_with: verifyWith } : {}),
|
|
5235
5629
|
};
|
|
5236
5630
|
const summaryParts = [`✔ bclaw_coordinate [${req.intent}] targets=${resolvedAgents.length}`];
|
|
5237
5631
|
if (resultExecStatus)
|
|
@@ -5247,7 +5641,8 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5247
5641
|
}
|
|
5248
5642
|
if (name === 'bclaw_loop') {
|
|
5249
5643
|
const { handleBclawLoop } = await import('./loops-handlers.js');
|
|
5250
|
-
const
|
|
5644
|
+
const targetCwd = resolveProjectCwd(args?.project, cwd);
|
|
5645
|
+
const result = handleBclawLoop({ args: args, cwd: targetCwd });
|
|
5251
5646
|
return {
|
|
5252
5647
|
response: toolResponse({
|
|
5253
5648
|
content: [{ type: 'text', text: result.summary }],
|
|
@@ -5321,10 +5716,12 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5321
5716
|
// applied when it hadn't. Under the new contract, an unknown key is
|
|
5322
5717
|
// a validation_error listing the keys actually honored.
|
|
5323
5718
|
const KNOWN_FILTER_KEYS = new Set([
|
|
5324
|
-
'status', 'tag', 'author', 'plan_id', 'source', 'auto_generated',
|
|
5719
|
+
'status', 'tag', 'tags', 'author', 'plan_id', 'source', 'auto_generated',
|
|
5720
|
+
'assignment_id', 'claim_id', 'message_id',
|
|
5325
5721
|
'limit', 'offset', 'includeLegacy', 'minAutoReflectConfidence',
|
|
5326
5722
|
]);
|
|
5327
|
-
const
|
|
5723
|
+
const agentRunOnlyFilterKeys = new Set(['assignment_id', 'claim_id', 'message_id']);
|
|
5724
|
+
const unknownKeys = Object.keys(filter).filter((k) => !KNOWN_FILTER_KEYS.has(k) || (agentRunOnlyFilterKeys.has(k) && entity !== 'agent_run'));
|
|
5328
5725
|
if (unknownKeys.length > 0) {
|
|
5329
5726
|
return {
|
|
5330
5727
|
response: createToolErrorResponse('validation_error', `Unknown filter key(s): ${unknownKeys.map((k) => `"${k}"`).join(', ')}. ` +
|
|
@@ -5332,6 +5729,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5332
5729
|
};
|
|
5333
5730
|
}
|
|
5334
5731
|
const result = listEntities(entity, targetCwd, filter);
|
|
5732
|
+
const warnings = collectLoadValidationWarnings(entity, targetCwd);
|
|
5335
5733
|
// structuredContent is the canonical MCP return channel that clients
|
|
5336
5734
|
// (VS Code extension, Codex, etc.) read for machine-parseable data.
|
|
5337
5735
|
// Prior to this fix we spread `...result` at top-level of the
|
|
@@ -5341,7 +5739,7 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5341
5739
|
return {
|
|
5342
5740
|
response: toolResponse({
|
|
5343
5741
|
content: [{ type: 'text', text: `✔ ${result.total} ${entity} item(s)` }],
|
|
5344
|
-
structuredContent: { ...result },
|
|
5742
|
+
structuredContent: { ...result, warnings },
|
|
5345
5743
|
}),
|
|
5346
5744
|
};
|
|
5347
5745
|
}
|
|
@@ -5354,6 +5752,21 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
5354
5752
|
const entity = String(args.entity ?? '');
|
|
5355
5753
|
const id = String(args.id ?? '');
|
|
5356
5754
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
5755
|
+
const validationWarning = findLoadValidationWarning(entity, id, targetCwd);
|
|
5756
|
+
if (validationWarning) {
|
|
5757
|
+
return {
|
|
5758
|
+
response: toolResponse({
|
|
5759
|
+
content: [{ type: 'text', text: `✖ ${entity} ${id} failed validation at load` }],
|
|
5760
|
+
structuredContent: {
|
|
5761
|
+
ok: false,
|
|
5762
|
+
error: 'validation_failed',
|
|
5763
|
+
entity_id: validationWarning.entity_id,
|
|
5764
|
+
validation_errors: validationWarning.validation_errors,
|
|
5765
|
+
path: validationWarning.path,
|
|
5766
|
+
},
|
|
5767
|
+
}, true),
|
|
5768
|
+
};
|
|
5769
|
+
}
|
|
5357
5770
|
const item = getEntity(entity, id, targetCwd);
|
|
5358
5771
|
return {
|
|
5359
5772
|
response: toolResponse({
|
|
@@ -5514,7 +5927,7 @@ export async function executeMcpToolCall(payload) {
|
|
|
5514
5927
|
}
|
|
5515
5928
|
else {
|
|
5516
5929
|
// First-ever connection for this claim — start a fresh session + adopt.
|
|
5517
|
-
const sessionResult = startSession({ cwd, maintenanceMode: 'fast' });
|
|
5930
|
+
const sessionResult = await startSession({ cwd, maintenanceMode: 'fast' });
|
|
5518
5931
|
autoSessionId = sessionResult.session_id;
|
|
5519
5932
|
effectiveConnectionSessionId = autoSessionId;
|
|
5520
5933
|
try {
|
|
@@ -5545,6 +5958,12 @@ export async function executeMcpToolCall(payload) {
|
|
|
5545
5958
|
}
|
|
5546
5959
|
catch { /* best-effort */ }
|
|
5547
5960
|
}
|
|
5961
|
+
if ((payload.name === 'bclaw_find' || payload.name === 'bclaw_get') && payload.args.entity === 'agent_run') {
|
|
5962
|
+
try {
|
|
5963
|
+
sweepDeadPidRunningAgentRunsAtRead(cwd);
|
|
5964
|
+
}
|
|
5965
|
+
catch { /* best-effort */ }
|
|
5966
|
+
}
|
|
5548
5967
|
// ── Delegate to inner handler ───────────────────────────────────────────────
|
|
5549
5968
|
const outcome = await _executeMcpToolCallInner({
|
|
5550
5969
|
...payload,
|