brainclaw 1.28.3 → 1.28.4
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 +6 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/harvest.js +67 -25
- package/dist/commands/loops-handlers.js +28 -1
- package/dist/commands/mcp-catalog.js +23 -2
- package/dist/commands/mcp-read-handlers.js +15 -1
- package/dist/commands/mcp-schemas.generated.js +13 -0
- package/dist/commands/mcp-write-coordination.js +84 -22
- package/dist/commands/mcp-write-memory.js +87 -1
- package/dist/commands/mcp.js +16 -2
- package/dist/core/context.js +16 -3
- package/dist/core/dispatch-status.js +36 -14
- package/dist/core/dispatcher.js +28 -20
- package/dist/core/entity-operations.js +62 -4
- package/dist/core/entity-registry.js +3 -3
- package/dist/core/execution-adapters.js +10 -0
- package/dist/core/facade-schema.js +10 -0
- package/dist/core/ideation-loop-close.js +3 -1
- package/dist/core/lane-result-file.js +72 -0
- package/dist/core/loop-turn-dispatch.js +2 -0
- package/dist/core/loops/brief-assembly.js +19 -11
- package/dist/core/loops/next-expected.js +56 -1
- package/dist/core/loops/reconcile-turn.js +8 -0
- package/dist/core/loops/result-reducers.js +14 -12
- package/dist/core/loops/store.js +4 -0
- package/dist/core/loops/types.js +14 -2
- package/dist/core/loops/verbs.js +8 -1
- package/dist/core/loops/worker-reply-contract.js +1 -1
- package/dist/core/protocol-tool-policy.js +1 -0
- package/dist/core/review-loop-turn-dispatch.js +1 -0
- package/dist/core/schema.js +24 -1
- package/dist/core/search.js +3 -2
- package/dist/core/worktree.js +14 -7
- package/dist/facts.js +9 -8
- package/dist/facts.json +8 -7
- package/docs/cli.md +33 -0
- package/docs/concepts/ideation-loop.md +35 -14
- package/docs/integrations/mcp.md +13 -3
- package/docs/mcp-schema-changelog.md +42 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -282,6 +282,12 @@ per-phase memory filters. The shared controls are `open`, `turn`,
|
|
|
282
282
|
implementation also adds `bind` and `verify`. `request_input` /
|
|
283
283
|
`provide_input` are cross-cutting clarification primitives for any workflow.
|
|
284
284
|
|
|
285
|
+
Ideation is sequential and conversational by default: critic B sees and
|
|
286
|
+
challenges critic A's contribution, critic C sees both, then the champion
|
|
287
|
+
revises and the ordered participants run another bounded round. The three
|
|
288
|
+
critic slots may all use the same installed agent with distinct persisted
|
|
289
|
+
perspectives; `ideation_schedule="parallel"` is an explicit fan-out option.
|
|
290
|
+
|
|
285
291
|
Every worker-backed phase, across all five workflows, is launched from one
|
|
286
292
|
immutable [execution contract](docs/concepts/execution-contract.md): exact
|
|
287
293
|
identity, artifact expectations, workspace policy, capability snapshot and
|
|
Binary file
|
package/dist/commands/harvest.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
|
-
import { CandidateSchema
|
|
16
|
+
import { CandidateSchema } from '../core/schema.js';
|
|
17
17
|
import { gitEvidence } from '../core/dispatch-status.js';
|
|
18
18
|
import { listCandidates, listArchivedCandidates, saveCandidate } from '../core/candidates.js';
|
|
19
19
|
import { createRuntimeEvent } from '../core/events.js';
|
|
@@ -26,11 +26,14 @@ import { closeReviewLoopFromLaneResult } from '../core/review-loop-close.js';
|
|
|
26
26
|
import { closeIdeationLoopFromLaneResult } from '../core/ideation-loop-close.js';
|
|
27
27
|
import { dispatchReviewLoopTurn, turnOwnedLoopEnabled } from '../core/review-loop-turn-dispatch.js';
|
|
28
28
|
import { reconcileTurnOwnedLane, turnOwnedLaneEvidence } from '../core/loops/reconcile-turn.js';
|
|
29
|
+
import { findReservationByAssignmentId } from '../core/loops/attempt-reservation.js';
|
|
29
30
|
import { getLoop } from '../core/loops/store.js';
|
|
31
|
+
import { computeNextExpected } from '../core/loops/next-expected.js';
|
|
30
32
|
import { phasePolicy } from '../core/loops/kind-policies.js';
|
|
31
33
|
import { reconcileClaimConformity } from '../core/claim-conformity.js';
|
|
32
34
|
import { toWarningDetail } from '../core/warnings.js';
|
|
33
35
|
import { harvestHarnessObservation } from '../core/harness-adapters/index.js';
|
|
36
|
+
import { LANE_RESULT_FILENAME, resolveLaneResultFile } from '../core/lane-result-file.js';
|
|
34
37
|
/**
|
|
35
38
|
* pln#630 PR3a — finalize a TURN-OWNED review lane via the exactly-once `reconcileTurn`
|
|
36
39
|
* instead of the legacy `closeReviewLoopFromLaneResult`. Returns `undefined` for a legacy
|
|
@@ -349,7 +352,7 @@ export function runHarvestCandidates(options = {}) {
|
|
|
349
352
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
350
353
|
/** Conventional path of a worker's lane-result file at the worktree root. */
|
|
351
354
|
export function getLaneResultPath(worktreePath) {
|
|
352
|
-
return path.join(worktreePath,
|
|
355
|
+
return path.join(worktreePath, LANE_RESULT_FILENAME);
|
|
353
356
|
}
|
|
354
357
|
/** Idempotency marker so a lane-result is harvested once. */
|
|
355
358
|
function laneHarvestedMarkerPath(cwd, assignmentId) {
|
|
@@ -365,11 +368,19 @@ function laneHarvestedMarkerPath(cwd, assignmentId) {
|
|
|
365
368
|
export function harvestLaneResults(options = {}) {
|
|
366
369
|
const cwd = options.cwd ?? process.cwd();
|
|
367
370
|
const agent = options.agent ?? 'coordinator';
|
|
368
|
-
const result = { harvested: [], skipped: [], errors: [], warnings: [] };
|
|
371
|
+
const result = { harvested: [], skipped: [], errors: [], warnings: [], continuations: [] };
|
|
369
372
|
const worktreePaths = resolveLaneScanPaths(options, cwd);
|
|
370
373
|
for (const worktreePath of worktreePaths) {
|
|
371
|
-
const
|
|
372
|
-
|
|
374
|
+
const fileResolution = resolveLaneResultFile(worktreePath, options.assignmentId);
|
|
375
|
+
if (fileResolution.kind === 'invalid') {
|
|
376
|
+
result.errors.push(`Failed to parse ${fileResolution.path}: ${fileResolution.error}`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (fileResolution.kind === 'ambiguous') {
|
|
380
|
+
result.errors.push(`Ambiguous lane-result files for ${options.assignmentId ?? worktreePath}: ${fileResolution.paths.join(', ')}`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const fileExists = fileResolution.kind === 'found';
|
|
373
384
|
let nativeObservation;
|
|
374
385
|
if (options.assignmentId) {
|
|
375
386
|
try {
|
|
@@ -384,13 +395,7 @@ export function harvestLaneResults(options = {}) {
|
|
|
384
395
|
continue;
|
|
385
396
|
let lane;
|
|
386
397
|
if (fileExists) {
|
|
387
|
-
|
|
388
|
-
lane = LaneResultSchema.parse(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
389
|
-
}
|
|
390
|
-
catch (err) {
|
|
391
|
-
result.errors.push(`Failed to parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
398
|
+
lane = fileResolution.lane;
|
|
394
399
|
}
|
|
395
400
|
else {
|
|
396
401
|
lane = nativeObservation.lane;
|
|
@@ -582,6 +587,26 @@ export function harvestLaneResults(options = {}) {
|
|
|
582
587
|
}
|
|
583
588
|
}
|
|
584
589
|
result.harvested.push(lane);
|
|
590
|
+
if (!options.dryRun) {
|
|
591
|
+
const reservation = findReservationByAssignmentId(lane.assignment_id, cwd);
|
|
592
|
+
if (reservation) {
|
|
593
|
+
try {
|
|
594
|
+
const loop = getLoop(reservation.loop_id, cwd);
|
|
595
|
+
if (loop) {
|
|
596
|
+
result.continuations.push({
|
|
597
|
+
assignment_id: lane.assignment_id,
|
|
598
|
+
loop_id: loop.id,
|
|
599
|
+
next_expected: computeNextExpected(loop),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// Reconciliation already reports corrupt/unreadable loop state as a
|
|
605
|
+
// loud warning. Continuation hints are best-effort and must not turn
|
|
606
|
+
// a successfully harvested result into an uncaught failure.
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
585
610
|
}
|
|
586
611
|
return result;
|
|
587
612
|
}
|
|
@@ -662,17 +687,18 @@ export function integrateLaneResults(options = {}) {
|
|
|
662
687
|
const result = { integrated: [], skipped: [], errors: [], next_turns: [] };
|
|
663
688
|
const worktreePaths = resolveLaneScanPaths(options, cwd);
|
|
664
689
|
for (const worktreePath of worktreePaths) {
|
|
665
|
-
const
|
|
666
|
-
if (
|
|
690
|
+
const fileResolution = resolveLaneResultFile(worktreePath, options.assignmentId);
|
|
691
|
+
if (fileResolution.kind === 'absent')
|
|
692
|
+
continue;
|
|
693
|
+
if (fileResolution.kind === 'invalid') {
|
|
694
|
+
result.errors.push(`Failed to parse ${fileResolution.path}: ${fileResolution.error}`);
|
|
667
695
|
continue;
|
|
668
|
-
let lane;
|
|
669
|
-
try {
|
|
670
|
-
lane = LaneResultSchema.parse(JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
671
696
|
}
|
|
672
|
-
|
|
673
|
-
result.errors.push(`
|
|
697
|
+
if (fileResolution.kind === 'ambiguous') {
|
|
698
|
+
result.errors.push(`Ambiguous lane-result files for ${options.assignmentId ?? worktreePath}: ${fileResolution.paths.join(', ')}`);
|
|
674
699
|
continue;
|
|
675
700
|
}
|
|
701
|
+
const lane = fileResolution.lane;
|
|
676
702
|
if (options.assignmentId && lane.assignment_id !== options.assignmentId)
|
|
677
703
|
continue;
|
|
678
704
|
const assignment = loadAssignment(lane.assignment_id, cwd);
|
|
@@ -681,6 +707,11 @@ export function integrateLaneResults(options = {}) {
|
|
|
681
707
|
result.errors.push(`No assignment record for lane ${lane.assignment_id} — cannot integrate`);
|
|
682
708
|
continue;
|
|
683
709
|
}
|
|
710
|
+
const candidateTurnEvidence = turnOwnedLaneEvidence(lane, cwd);
|
|
711
|
+
const candidateOwnedLoop = candidateTurnEvidence ? getLoop(candidateTurnEvidence.reservation.loop_id, cwd) : undefined;
|
|
712
|
+
const ownedTurnEvidence = candidateTurnEvidence && candidateOwnedLoop && turnOwnedLoopEnabled(candidateOwnedLoop.kind)
|
|
713
|
+
? candidateTurnEvidence
|
|
714
|
+
: undefined;
|
|
684
715
|
const profile = getCapabilityProfile(assignment.agent);
|
|
685
716
|
// No profile ⇒ assume it can commit (conservative: don't author for an
|
|
686
717
|
// unknown agent), so brainclaw only lifecycles.
|
|
@@ -726,7 +757,12 @@ export function integrateLaneResults(options = {}) {
|
|
|
726
757
|
...(entry.commit_sha ? [{ type: 'commit', ref: entry.commit_sha, description: 'on-behalf integration commit' }] : []),
|
|
727
758
|
...entry.files_changed.slice(0, 50).map((f) => ({ type: 'file', ref: f })),
|
|
728
759
|
];
|
|
729
|
-
|
|
760
|
+
// Turn-owned lanes are terminalized only after their artifact contract
|
|
761
|
+
// passes reconcileTurn. A repairable envelope must not complete the
|
|
762
|
+
// Assignment or fail/release its slot before the corrected replay.
|
|
763
|
+
if (!ownedTurnEvidence) {
|
|
764
|
+
entry.assignment_completed = forceCompleteAssignment(lane.assignment_id, artifacts, `pln#534 on-behalf integration: ${lane.summary.slice(0, 120)}`, actor, cwd);
|
|
765
|
+
}
|
|
730
766
|
// pln#628 Focus 4B — map this lane onto its review loop BEFORE deciding
|
|
731
767
|
// teardown: PR1 records the verdict + advances (auto-close on approve);
|
|
732
768
|
// PR2 continues the fix cycle on request_changes (bump round, emit a
|
|
@@ -735,11 +771,8 @@ export function integrateLaneResults(options = {}) {
|
|
|
735
771
|
// for non-review lanes / lanes without a verdict; never throws.
|
|
736
772
|
// Legacy ideation lanes still use the historical closer. A turn-owned
|
|
737
773
|
// lane of any kind is finalized exactly once by reconcileTurn below.
|
|
738
|
-
const
|
|
739
|
-
const
|
|
740
|
-
const turnOwnedEvidence = candidateEvidence && ownedLoop && turnOwnedLoopEnabled(ownedLoop.kind)
|
|
741
|
-
? candidateEvidence
|
|
742
|
-
: undefined;
|
|
774
|
+
const ownedLoop = candidateOwnedLoop;
|
|
775
|
+
const turnOwnedEvidence = ownedTurnEvidence;
|
|
743
776
|
if (!turnOwnedEvidence) {
|
|
744
777
|
const ideationClose = closeIdeationLoopFromLaneResult(assignment, lane, actor, cwd);
|
|
745
778
|
if (ideationClose) {
|
|
@@ -781,6 +814,7 @@ export function integrateLaneResults(options = {}) {
|
|
|
781
814
|
if (rr.next_turn) {
|
|
782
815
|
result.next_turns.push({ loop_id: reservation.loop_id, ...rr.next_turn });
|
|
783
816
|
}
|
|
817
|
+
entry.assignment_completed = loadAssignment(lane.assignment_id, cwd)?.status === 'completed';
|
|
784
818
|
// Claim/run/assignment settling is OWNED by reconcileTurn, so we do NOT run the
|
|
785
819
|
// legacy teardown gate — just reflect the resulting claim state. Settlement
|
|
786
820
|
// semantics (reconcile-turn.ts, review #1): an ACCEPTED lane — approve OR
|
|
@@ -1221,6 +1255,7 @@ export async function runHarvestLane(assignmentId, options = {}) {
|
|
|
1221
1255
|
// collected but never emitted on ANY channel; the silent half of the
|
|
1222
1256
|
// 2026-08-02/03 review-loop stalls.
|
|
1223
1257
|
warnings: result.warnings,
|
|
1258
|
+
continuations: result.continuations,
|
|
1224
1259
|
}, null, 2));
|
|
1225
1260
|
return;
|
|
1226
1261
|
}
|
|
@@ -1257,6 +1292,13 @@ export async function runHarvestLane(assignmentId, options = {}) {
|
|
|
1257
1292
|
for (const w of result.warnings) {
|
|
1258
1293
|
console.log(` ⚠ ${w.message}`);
|
|
1259
1294
|
}
|
|
1295
|
+
for (const continuation of result.continuations) {
|
|
1296
|
+
const next = continuation.next_expected;
|
|
1297
|
+
if (!next)
|
|
1298
|
+
continue;
|
|
1299
|
+
const slot = next.slot_id ? ` slot=${next.slot_id}` : '';
|
|
1300
|
+
console.log(` ↻ Next loop action [${continuation.loop_id}]: ${next.action}${slot}${next.reason ? ` — ${next.reason}` : ''}`);
|
|
1301
|
+
}
|
|
1260
1302
|
const warnTag = result.warnings.length > 0 ? `, ${result.warnings.length} warning(s)` : '';
|
|
1261
1303
|
console.log(`\n✔ Lane harvest complete${dryTag}: ${result.harvested.length} harvested, ${result.skipped.length} skipped, ${result.errors.length} error(s)${warnTag}.`);
|
|
1262
1304
|
}
|
|
@@ -23,12 +23,15 @@ function successResponse(intent, result, artifacts, side_effects, warnings, dura
|
|
|
23
23
|
const resultLoop = result && typeof result === 'object' && 'loop' in result
|
|
24
24
|
? result.loop
|
|
25
25
|
: undefined;
|
|
26
|
+
const enrichedResult = resultLoop && result && typeof result === 'object'
|
|
27
|
+
? { ...result, progress: loopProgress(resultLoop) }
|
|
28
|
+
: result;
|
|
26
29
|
const nextActions = resultLoop ? pipelineNextActions(resultLoop) : [];
|
|
27
30
|
return {
|
|
28
31
|
response: {
|
|
29
32
|
status: 'ok',
|
|
30
33
|
intent: `bclaw_loop.${intent}`,
|
|
31
|
-
result,
|
|
34
|
+
result: enrichedResult,
|
|
32
35
|
artifacts,
|
|
33
36
|
side_effects,
|
|
34
37
|
warnings,
|
|
@@ -86,6 +89,30 @@ function pipelineNextActions(loop) {
|
|
|
86
89
|
}
|
|
87
90
|
return [];
|
|
88
91
|
}
|
|
92
|
+
function loopProgress(loop) {
|
|
93
|
+
const phase = loop.phases.find((candidate) => candidate.name === loop.current_phase);
|
|
94
|
+
const phaseSlots = loop.slots.filter((slot) => (slot.phase ?? loop.current_phase) === loop.current_phase);
|
|
95
|
+
const slotCounts = {};
|
|
96
|
+
for (const slot of phaseSlots)
|
|
97
|
+
slotCounts[slot.status] = (slotCounts[slot.status] ?? 0) + 1;
|
|
98
|
+
const artifactCounts = {};
|
|
99
|
+
for (const artifact of loop.artifacts.filter((item) => item.phase === loop.current_phase)) {
|
|
100
|
+
artifactCounts[artifact.type] = (artifactCounts[artifact.type] ?? 0) + 1;
|
|
101
|
+
}
|
|
102
|
+
const gate = evaluatePhaseAdvanceGate(loop, phase?.advance_gate);
|
|
103
|
+
const activeSlots = phaseSlots.filter((slot) => ['open', 'assigned', 'working', 'waiting_input'].includes(slot.status));
|
|
104
|
+
const stuck = loop.status === 'open' && !gate.advance && activeSlots.length === 0;
|
|
105
|
+
return {
|
|
106
|
+
phase: loop.current_phase,
|
|
107
|
+
iteration: loop.iteration_count,
|
|
108
|
+
slots_by_status: slotCounts,
|
|
109
|
+
artifacts_by_type: artifactCounts,
|
|
110
|
+
gate_met: gate.advance,
|
|
111
|
+
...(gate.gate_reason ? { gate_reason: gate.gate_reason } : {}),
|
|
112
|
+
stuck,
|
|
113
|
+
...(stuck ? { recovery: 'Replay a real slot turn with bclaw_loop(intent="turn", slot_id=…); add_artifact cannot satisfy strict evidence.' } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
89
116
|
/** Concrete action evaluated by continuation policy; never exposed as an ungoverned hint. */
|
|
90
117
|
function proposedPipelineActions(loop, cwd) {
|
|
91
118
|
if (loop.kind === 'ideation') {
|
|
@@ -848,10 +848,12 @@ const MCP_WRITE_TOOLS = [
|
|
|
848
848
|
inputSchema: {
|
|
849
849
|
type: 'object',
|
|
850
850
|
properties: {
|
|
851
|
-
intent: { type: 'string', enum: ['assign', 'consult', 'review', 'reroute', 'summarize', 'ideate'], description: 'Coordination intent. assign/review/reroute and multi-agent ideate spawn worker processes; consult/summarize do not. "assign" creates a claim per target agent and spawns a worker on the brief. "consult" delivers the brief to the target inbox(es) WITHOUT creating claims and WITHOUT spawning — targets pick it up via their own bclaw_work. "review" creates a review candidate (and, with open_loop, a review loop). "ideate" opens an ideation loop with the task as the proposal seed; with targetAgents it advances to critique and
|
|
851
|
+
intent: { type: 'string', enum: ['assign', 'consult', 'review', 'reroute', 'summarize', 'ideate'], description: 'Coordination intent. assign/review/reroute and multi-agent ideate spawn worker processes; consult/summarize do not. "assign" creates a claim per target agent and spawns a worker on the brief. "consult" delivers the brief to the target inbox(es) WITHOUT creating claims and WITHOUT spawning — targets pick it up via their own bclaw_work. "review" creates a review candidate (and, with open_loop, a review loop). "ideate" opens an ideation loop with the task as the proposal seed; with targetAgents it advances to critique and, by default, starts one critic at a time. Set ideation_schedule="parallel" for immediate fan-out. "reroute" releases the current claim and reassigns. "summarize" reads a thread and returns a summary.' },
|
|
852
852
|
task: { type: 'string', description: 'Brief or task description delivered to target agents. TRANSPORT NOTE (dec#133): a spawned worker\'s capabilities follow its invoke template, not the mere presence of "sandbox". A sandboxed codex worker (`--sandbox workspace-write`, `approval_policy=never`) CAN reach brainclaw MCP — the server runs out-of-sandbox and every tool call is auto-approved — so MCP lifecycle calls (`bclaw_assignment_update`, `bclaw_send_message`, …) do NOT hang. Its one real limit is that `.git` is read-only: it cannot `git commit`, so it must leave fixes uncommitted in the worktree and the coordinator integrates + commits the diff at harvest (never instruct such a worker to commit). Genuinely MCP-less agents (nanoclaw/nemoclaw/picoclaw/zeroclaw) have no MCP at all: for them, prefer file-based protocols (write findings/reply to a markdown file in the worktree; the coordinator harvests it and lifecycle-closes the assignment). See docs/integrations/<agent>.md for the per-agent capability matrix.' },
|
|
853
853
|
scope: { type: 'string', description: 'File or feature scope. Used as claim scope for assign/reroute; as thread id for summarize if threadId is absent.' },
|
|
854
854
|
targetAgents: { type: 'array', items: { type: 'string' }, description: 'Agent names to target. If omitted, all spawnable agents are used.' },
|
|
855
|
+
ideation_schedule: { type: 'string', enum: ['sequential', 'parallel'], description: 'For intent=ideate with targetAgents: sequential (default) starts only the first critic; after harvest, drive the next open critic with bclaw_loop turn dispatch=true. parallel starts all critics immediately.' },
|
|
856
|
+
criticPerspectives: { type: 'array', items: { type: 'string' }, description: 'Optional ideation instructions/lenses aligned positionally with targetAgents. If omitted, Brainclaw assigns distinct evidence, failure-mode, and alternative/trade-off lenses.' },
|
|
855
857
|
constraints: { type: 'object', description: 'Optional structured constraints passed alongside the brief (e.g. deadline, reviewCriteria).' },
|
|
856
858
|
threadId: { type: 'string', description: 'Thread ID for summarize intent.' },
|
|
857
859
|
linked: {
|
|
@@ -864,7 +866,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
864
866
|
},
|
|
865
867
|
additionalProperties: false,
|
|
866
868
|
},
|
|
867
|
-
autoExecute: { type: 'boolean', description: 'Attempt to spawn target agents after delivery (default: true). Applies to the spawning intents assign/review/reroute
|
|
869
|
+
autoExecute: { type: 'boolean', description: 'Attempt to spawn target agents after delivery (default: true). Applies to the spawning intents assign/review/reroute and multi-agent ideate. Sequential ideation starts one critic; parallel ideation starts every critic. consult is inbox-only and ignores autoExecute; summarize just reads a thread and ignores it. When false on a spawning intent, returns command_ready_manual with commands for the supervisor to run.' },
|
|
868
870
|
open_loop: { type: 'boolean', description: 'For intent=review only: also open a review Loop on top of the candidate (author + reviewer slots, advance to `findings`, dispatch turns). Default false — existing review callers are unaffected. See docs/concepts/loop-engine.md §Automation.' },
|
|
869
871
|
review_mode: { type: 'string', enum: ['asymmetric', 'symmetric'], description: 'Optional review Loop mode when open_loop=true. `asymmetric` (default) keeps the classical author→reviewer handoff; `symmetric` lets each reviewer turn also apply fixes directly, halving round-trips for spec/doc reviews. Ignored when open_loop is false.' },
|
|
870
872
|
preflight: { type: 'boolean', description: 'pln#533: when open_loop=true, run a trivial validation spawn per reviewer agent BEFORE opening the loop so an environment death (config rejected, auth fail, model mismatch) surfaces instantly with a clear reason instead of a generic loop timeout. Reviewers that fail pre-flight are dropped (with a targeted warning); if all fail, loop creation is skipped. Default true; set false to skip (e.g. you already ran `brainclaw doctor --spawn-check`). Ignored when open_loop is false or BRAINCLAW_NO_SPAWN is set.' },
|
|
@@ -999,6 +1001,24 @@ const MCP_WRITE_TOOLS = [
|
|
|
999
1001
|
required: [],
|
|
1000
1002
|
},
|
|
1001
1003
|
},
|
|
1004
|
+
{
|
|
1005
|
+
name: 'bclaw_harvest',
|
|
1006
|
+
description: 'Harvest worker LANE-RESULT.json envelopes into assignment/loop state. This is MCP parity for `brainclaw harvest` and is distinct from candidate-memory harvest. Use assignmentId for one lane or all=true; integrate=true also performs the CLI --integrate lifecycle. Repairable contract errors leave the loop slot replayable and are returned as warnings.',
|
|
1007
|
+
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
1008
|
+
inputSchema: {
|
|
1009
|
+
type: 'object',
|
|
1010
|
+
properties: {
|
|
1011
|
+
assignmentId: { type: 'string', description: 'One Assignment whose lane result should be harvested.' },
|
|
1012
|
+
all: { type: 'boolean', description: 'Scan every managed lane; mutually exclusive with assignmentId.' },
|
|
1013
|
+
worktreePaths: { type: 'array', items: { type: 'string' }, description: 'Optional explicit worktrees to scan.' },
|
|
1014
|
+
dryRun: { type: 'boolean', description: 'Report without writing state.' },
|
|
1015
|
+
integrate: { type: 'boolean', description: 'Also lifecycle/commit-on-behalf like CLI --integrate.' },
|
|
1016
|
+
agent: { type: 'string', description: 'Coordinator agent name.' },
|
|
1017
|
+
agentId: { type: 'string', description: 'Registered coordinator agent id.' },
|
|
1018
|
+
},
|
|
1019
|
+
required: [],
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1002
1022
|
// ── Canonical CRUD verbs (Phase 3 / v1.0 grammar) ──────────────────
|
|
1003
1023
|
// Promoted to `standard` tier at the v1.0 cut.
|
|
1004
1024
|
{
|
|
@@ -1012,6 +1032,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1012
1032
|
filter: { type: 'object', description: 'Filter keys (ANY entity): 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-SCOPED keys (rejected with a validation_error if used with any other entity): assignment_id, claim_id, message_id — ONLY for entity="agent_run"; scope ("project" default | "global", the latter unions the dispatchable catalog + adds dispatchable/registered) and includeReputation (bool — attaches a public reputation summary per agent) — ONLY for entity="agent". Unknown/mis-scoped keys are rejected loudly.' },
|
|
1013
1033
|
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`.' },
|
|
1014
1034
|
budget_tokens: { type: 'number', description: 'Optional token budget for the page payload (~4 chars/token). Tightens the default size cap; pagination metadata (has_more/next_offset) still applies.' },
|
|
1035
|
+
fields: { type: 'array', items: { type: 'string' }, description: 'Optional field projection for each row, e.g. ["id","status","created_at"].' },
|
|
1015
1036
|
},
|
|
1016
1037
|
required: ['entity'],
|
|
1017
1038
|
},
|
|
@@ -232,6 +232,13 @@ function dispatchReadTool(name, args, ctx) {
|
|
|
232
232
|
notifications = buildNotificationSummary(unseenEvents);
|
|
233
233
|
unseenEventCount = unseenEvents.length;
|
|
234
234
|
}
|
|
235
|
+
const actionableNotificationTypes = new Set(['action', 'assignment', 'claim', 'plan', 'handoff', 'candidate', 'loop']);
|
|
236
|
+
const actionableNotifications = notifications
|
|
237
|
+
? Object.fromEntries(Object.entries(notifications).filter(([key]) => actionableNotificationTypes.has(key.split(':').at(-1) ?? '')))
|
|
238
|
+
: undefined;
|
|
239
|
+
const actionableCount = actionableNotifications
|
|
240
|
+
? Object.values(actionableNotifications).reduce((sum, count) => sum + count, 0)
|
|
241
|
+
: 0;
|
|
235
242
|
return {
|
|
236
243
|
content: [{ type: 'text', text: enrichedContent || 'No relevant memory found.' }],
|
|
237
244
|
structuredContent: {
|
|
@@ -246,7 +253,14 @@ function dispatchReadTool(name, args, ctx) {
|
|
|
246
253
|
name: tool.name,
|
|
247
254
|
type: tool.type,
|
|
248
255
|
})),
|
|
249
|
-
...(notifications ? {
|
|
256
|
+
...(notifications ? {
|
|
257
|
+
pending_notifications: {
|
|
258
|
+
actionable_count: actionableCount,
|
|
259
|
+
by_type: actionableNotifications ?? {},
|
|
260
|
+
telemetry_events_omitted: Math.max(0, (unseenEventCount ?? 0) - actionableCount),
|
|
261
|
+
},
|
|
262
|
+
unseen_event_count: unseenEventCount,
|
|
263
|
+
} : {}),
|
|
250
264
|
},
|
|
251
265
|
};
|
|
252
266
|
}
|
|
@@ -274,6 +274,11 @@ export const generatedSchemas = {
|
|
|
274
274
|
"agent_id": {
|
|
275
275
|
"type": "string"
|
|
276
276
|
},
|
|
277
|
+
"perspective": {
|
|
278
|
+
"type": "string",
|
|
279
|
+
"minLength": 1,
|
|
280
|
+
"maxLength": 1000
|
|
281
|
+
},
|
|
277
282
|
"assignment_id": {
|
|
278
283
|
"type": "string"
|
|
279
284
|
},
|
|
@@ -317,6 +322,14 @@ export const generatedSchemas = {
|
|
|
317
322
|
},
|
|
318
323
|
"current_turn_id": {
|
|
319
324
|
"type": "string"
|
|
325
|
+
},
|
|
326
|
+
"last_completed_phase": {
|
|
327
|
+
"type": "string"
|
|
328
|
+
},
|
|
329
|
+
"last_completed_iteration": {
|
|
330
|
+
"type": "integer",
|
|
331
|
+
"minimum": 0,
|
|
332
|
+
"maximum": 9007199254740991
|
|
320
333
|
}
|
|
321
334
|
},
|
|
322
335
|
"required": [
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
import crypto from 'node:crypto';
|
|
15
|
+
import fs from 'node:fs';
|
|
15
16
|
import path from 'node:path';
|
|
16
17
|
import { spawnSync } from 'node:child_process';
|
|
17
18
|
import { buildClaimEnvPrefix } from '../core/execution-profile.js';
|
|
@@ -22,8 +23,7 @@ import { appendAuditEntry } from '../core/audit.js';
|
|
|
22
23
|
import { nowISO } from '../core/ids.js';
|
|
23
24
|
import { validateMcpField } from '../core/input-validation.js';
|
|
24
25
|
import { generateCandidateIdWithLabel, saveCandidate } from '../core/candidates.js';
|
|
25
|
-
import { DEFAULT_PROTOCOLS } from '../core/loops/types.js';
|
|
26
|
-
import { capLoopArtifactBody } from '../core/loops/result-reducers.js';
|
|
26
|
+
import { DEFAULT_PROTOCOLS, LOOP_PROPOSAL_BODY_MAX_BYTES } from '../core/loops/types.js';
|
|
27
27
|
import { validateLoopProjectResolution } from '../core/loops/project-resolution.js';
|
|
28
28
|
import { coordinateNextActions, dispatchNextActions } from '../core/next-actions.js';
|
|
29
29
|
import { agentValidationFailedWarning, consultAutoExecuteNoOpWarning, planAlreadyAssignedWarning, pushStructuredWarning, scopeAlreadyClaimedWarning, } from '../core/warnings.js';
|
|
@@ -309,6 +309,25 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
309
309
|
return { response: createToolErrorResponse('validation_error', parseResult.error.message) };
|
|
310
310
|
}
|
|
311
311
|
const req = parseResult.data;
|
|
312
|
+
// A proposal is the caller's task contract. Silently replacing its tail with
|
|
313
|
+
// memory changed the questions workers answered during DGX dogfooding. Keep
|
|
314
|
+
// the whole task or reject before mutation; never truncate it in-band.
|
|
315
|
+
if (req.intent === 'ideate') {
|
|
316
|
+
const taskBytes = Buffer.byteLength(req.task, 'utf8');
|
|
317
|
+
if (taskBytes > LOOP_PROPOSAL_BODY_MAX_BYTES) {
|
|
318
|
+
return {
|
|
319
|
+
response: createToolErrorResponse('ideate_task_too_large', `ideation task is ${taskBytes} bytes; the lossless limit is ${LOOP_PROPOSAL_BODY_MAX_BYTES} bytes. Shorten it or attach a referenced artifact before retrying; no loop was created.`, { task_bytes: taskBytes, task_limit_bytes: LOOP_PROPOSAL_BODY_MAX_BYTES, task_truncated: false }),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (req.criticPerspectives) {
|
|
323
|
+
const targetCount = req.targetAgents?.length ?? 0;
|
|
324
|
+
if (targetCount === 0 || req.criticPerspectives.length !== targetCount) {
|
|
325
|
+
return {
|
|
326
|
+
response: createToolErrorResponse('ideate_perspective_count_mismatch', `criticPerspectives must contain exactly one instruction per targetAgents entry (targets=${targetCount}, perspectives=${req.criticPerspectives.length}); no loop was created.`),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
312
331
|
// pln#511 step 2 — preset selector validation. Presets are kind-
|
|
313
332
|
// specific in v1: only intent='ideate' carries them. Unknown names
|
|
314
333
|
// are rejected up-front against the registry so the handler never
|
|
@@ -427,8 +446,9 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
427
446
|
// pln#692 P0 — admission must prove that a multi-agent ideation request can
|
|
428
447
|
// satisfy the first worker-produced phase gate BEFORE openLoop (or any
|
|
429
448
|
// identity/claim/assignment mutation). The default critique phase requires
|
|
430
|
-
// three distinct critique artifacts
|
|
431
|
-
//
|
|
449
|
+
// three distinct critique artifacts. Capacity is therefore counted per
|
|
450
|
+
// requested critic INSTANCE, not per unique agent identity: each occurrence
|
|
451
|
+
// becomes an isolated slot with its own claim, worktree and turn authority.
|
|
432
452
|
if (req.intent === 'ideate'
|
|
433
453
|
&& req.preset !== 'bootstrap'
|
|
434
454
|
&& Array.isArray(req.targetAgents)
|
|
@@ -436,23 +456,27 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
436
456
|
const critiquePhase = DEFAULT_PROTOCOLS.ideation.phases.find((phase) => phase.name === 'critique');
|
|
437
457
|
const gate = critiquePhase?.advance_gate;
|
|
438
458
|
const requiredCritics = gate?.kind === 'min_artifacts_by_type' ? gate.n : 0;
|
|
439
|
-
const
|
|
440
|
-
const checks = uniqueTargets.map((agent) => ({
|
|
459
|
+
const checks = req.targetAgents.map((agent, instanceIndex) => ({
|
|
441
460
|
agent,
|
|
461
|
+
instanceIndex,
|
|
442
462
|
check: validateAgentForDispatch(agent, { requireSpawnable: true }),
|
|
443
463
|
}));
|
|
444
464
|
const executableTargets = checks.filter(({ check }) => check.valid).map(({ agent }) => agent);
|
|
445
|
-
const invalidTargets = checks.filter(({ check }) => !check.valid).map(({ agent, check }) => ({
|
|
465
|
+
const invalidTargets = checks.filter(({ check }) => !check.valid).map(({ agent, instanceIndex, check }) => ({
|
|
446
466
|
agent,
|
|
467
|
+
instance_index: instanceIndex,
|
|
447
468
|
code: check.code,
|
|
448
469
|
reason: check.reason,
|
|
449
470
|
}));
|
|
450
471
|
if (requiredCritics > 0 && executableTargets.length < requiredCritics) {
|
|
451
472
|
const availableTargets = getSpawnableAgents()
|
|
452
473
|
.map((profile) => profile.name)
|
|
453
|
-
.filter((agent, index, all) =>
|
|
474
|
+
.filter((agent, index, all) => all.indexOf(agent) === index)
|
|
454
475
|
.filter((agent) => validateAgentForDispatch(agent, { requireSpawnable: true }).valid);
|
|
455
|
-
const
|
|
476
|
+
const recoveryAgent = executableTargets[0] ?? availableTargets[0];
|
|
477
|
+
const recoveryTargets = recoveryAgent
|
|
478
|
+
? Array.from({ length: requiredCritics }, () => recoveryAgent)
|
|
479
|
+
: [];
|
|
456
480
|
const nextActions = recoveryTargets.length >= requiredCritics
|
|
457
481
|
? [{
|
|
458
482
|
tool: 'bclaw_coordinate',
|
|
@@ -460,21 +484,20 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
460
484
|
intent: 'ideate', task: req.task, scope: req.scope,
|
|
461
485
|
targetAgents: recoveryTargets, autoExecute: effectiveAutoExecute !== false,
|
|
462
486
|
},
|
|
463
|
-
when: `retry with at least ${requiredCritics}
|
|
487
|
+
when: `retry with at least ${requiredCritics} executable critic instances`,
|
|
464
488
|
}]
|
|
465
489
|
: [{
|
|
466
490
|
tool: 'bclaw_context',
|
|
467
491
|
args: { kind: 'execution', includeAgentTooling: true },
|
|
468
|
-
when:
|
|
492
|
+
when: 'configure at least one spawnable critic identity before retrying',
|
|
469
493
|
}];
|
|
470
494
|
return {
|
|
471
|
-
response: createToolErrorResponse('ideate_gate_capacity_unavailable', `ideation admission refused before mutation: critique gate requires ${requiredCritics}
|
|
495
|
+
response: createToolErrorResponse('ideate_gate_capacity_unavailable', `ideation admission refused before mutation: critique gate requires ${requiredCritics} executable critic instance(s), observed ${executableTargets.length}`, {
|
|
472
496
|
gate: { phase: 'critique', kind: gate?.kind, expected: requiredCritics, observed: executableTargets.length },
|
|
473
497
|
requested_targets: req.targetAgents,
|
|
474
498
|
executable_targets: executableTargets,
|
|
475
499
|
invalid_targets: invalidTargets,
|
|
476
500
|
blockers: [
|
|
477
|
-
...(uniqueTargets.length < req.targetAgents.length ? ['duplicate target identities do not add executable capacity'] : []),
|
|
478
501
|
...(invalidTargets.length > 0 ? ['one or more requested targets are not spawnable'] : []),
|
|
479
502
|
`missing executable critic capacity: ${requiredCritics - executableTargets.length}`,
|
|
480
503
|
],
|
|
@@ -738,6 +761,18 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
738
761
|
contextEnvelope: options?.contextEnvelope,
|
|
739
762
|
});
|
|
740
763
|
};
|
|
764
|
+
const compactDeliveryEntry = (entry) => {
|
|
765
|
+
if (!entry.command || entry.command.length <= 2048)
|
|
766
|
+
return entry;
|
|
767
|
+
const dir = path.join(dispatchCwd, '.brainclaw', 'coordination', 'runtime', 'manual-commands');
|
|
768
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
769
|
+
const ext = entry.shell === 'cmd' ? 'cmd' : 'sh';
|
|
770
|
+
const ref = entry.assignment_id ?? entry.message_id;
|
|
771
|
+
const commandFile = path.join(dir, `${ref}.${ext}`);
|
|
772
|
+
fs.writeFileSync(commandFile, entry.command, { encoding: 'utf8', mode: 0o600 });
|
|
773
|
+
const { command, ...rest } = entry;
|
|
774
|
+
return { ...rest, command_file: commandFile, command_bytes: Buffer.byteLength(command, 'utf8') };
|
|
775
|
+
};
|
|
741
776
|
const toMessageSummary = (deliveryPlan) => deliveryPlan.map((entry) => ({
|
|
742
777
|
agent: entry.agent,
|
|
743
778
|
message_id: entry.message_id,
|
|
@@ -1846,11 +1881,19 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1846
1881
|
},
|
|
1847
1882
|
];
|
|
1848
1883
|
if (explicitTargets) {
|
|
1849
|
-
|
|
1884
|
+
const defaultPerspectives = [
|
|
1885
|
+
'Challenge assumptions and verify the proposal against concrete evidence.',
|
|
1886
|
+
'Focus on failure modes, operational risks, and recovery paths; challenge earlier contributions explicitly.',
|
|
1887
|
+
'Develop competing alternatives and compare their costs and trade-offs; resolve or sharpen earlier disagreements.',
|
|
1888
|
+
];
|
|
1889
|
+
for (const [index, agent] of req.targetAgents.entries()) {
|
|
1850
1890
|
const criticIdentity = findAgentIdentityByName(agent, dispatchCwd) ?? ensureAgentRegisteredForDispatch(agent, dispatchCwd);
|
|
1851
1891
|
slots.push({
|
|
1852
1892
|
role: 'critic',
|
|
1853
1893
|
agent,
|
|
1894
|
+
perspective: req.criticPerspectives?.[index]
|
|
1895
|
+
?? defaultPerspectives[index]
|
|
1896
|
+
?? `Challenge the conversation from an independent perspective ${index + 1}; avoid repeating prior contributions.`,
|
|
1854
1897
|
...(criticIdentity?.agent_id ? { agent_id: criticIdentity.agent_id } : {}),
|
|
1855
1898
|
});
|
|
1856
1899
|
}
|
|
@@ -1878,7 +1921,12 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1878
1921
|
stop_condition: presetSelected.stop_condition,
|
|
1879
1922
|
protocol: presetSelected.protocol,
|
|
1880
1923
|
}
|
|
1881
|
-
: {
|
|
1924
|
+
: {
|
|
1925
|
+
protocol: {
|
|
1926
|
+
iteration: DEFAULT_PROTOCOLS.ideation.iteration,
|
|
1927
|
+
ideation_schedule: req.ideation_schedule,
|
|
1928
|
+
},
|
|
1929
|
+
}),
|
|
1882
1930
|
}, dispatchCwd);
|
|
1883
1931
|
loopId = loop.id;
|
|
1884
1932
|
artifacts.push({ type: 'loop', id: loop.id });
|
|
@@ -1892,14 +1940,13 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1892
1940
|
// loop doesn't contain. The task text is already captured on
|
|
1893
1941
|
// the thread (title + goal).
|
|
1894
1942
|
if (!presetSelected) {
|
|
1895
|
-
const proposalBody = capLoopArtifactBody(req.task);
|
|
1896
1943
|
const updated = add_artifact({
|
|
1897
1944
|
id: loop.id,
|
|
1898
1945
|
actor: creatorActor,
|
|
1899
1946
|
artifact: {
|
|
1900
1947
|
phase: 'proposal',
|
|
1901
1948
|
type: 'proposal',
|
|
1902
|
-
body:
|
|
1949
|
+
body: req.task,
|
|
1903
1950
|
produced_by: creatorActor,
|
|
1904
1951
|
},
|
|
1905
1952
|
}, dispatchCwd);
|
|
@@ -1918,8 +1965,11 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1918
1965
|
};
|
|
1919
1966
|
}
|
|
1920
1967
|
} // end else (non-bootstrap open path)
|
|
1921
|
-
//
|
|
1922
|
-
//
|
|
1968
|
+
// Multi-agent ideation keeps artifact capacity separate from execution
|
|
1969
|
+
// concurrency. All requested critic slots are durable, but sequential is
|
|
1970
|
+
// the default scheduling policy: only the first open slot is dispatched
|
|
1971
|
+
// now, and the next one is taken after this result is harvested. Explicit
|
|
1972
|
+
// parallel mode retains the historical immediate fan-out.
|
|
1923
1973
|
//
|
|
1924
1974
|
// pln#511 step 2 — initial phase comes from the actual loop's
|
|
1925
1975
|
// first phase, not a hardcoded 'proposal'. Presets like bootstrap
|
|
@@ -1977,7 +2027,10 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1977
2027
|
throw new Error('ideate dispatch: loop disappeared after advance');
|
|
1978
2028
|
}
|
|
1979
2029
|
dispatchedPhase = advancedLoop.current_phase;
|
|
1980
|
-
const
|
|
2030
|
+
const allCriticSlots = advancedLoop.slots.filter((s) => s.role === 'critic');
|
|
2031
|
+
const criticSlots = req.ideation_schedule === 'parallel'
|
|
2032
|
+
? allCriticSlots
|
|
2033
|
+
: allCriticSlots.slice(0, 1);
|
|
1981
2034
|
for (const slot of criticSlots) {
|
|
1982
2035
|
if (!slot.agent)
|
|
1983
2036
|
continue;
|
|
@@ -1997,7 +2050,10 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1997
2050
|
const briefResult = buildIdeationBrief({
|
|
1998
2051
|
thread: advancedLoop,
|
|
1999
2052
|
slotRole: slot.role,
|
|
2053
|
+
slotPerspective: slot.perspective,
|
|
2000
2054
|
memoryProvider: provider,
|
|
2055
|
+
seedText: req.task,
|
|
2056
|
+
scopeHints: req.scope ? [req.scope] : [],
|
|
2001
2057
|
});
|
|
2002
2058
|
// pln#626 Phase 2 (Option B) — spawn the critic as a worktree-isolated
|
|
2003
2059
|
// worker, mirroring the intent=assign / review chain. Each critic gets
|
|
@@ -2009,7 +2065,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
2009
2065
|
// stray edit harmless (it lands in the throwaway checkout, not master).
|
|
2010
2066
|
const criticScope = `ideate-loop:${loopId}:${slot.slot_id}`;
|
|
2011
2067
|
const criticDescription = `Ideation critic turn for loop ${loopId} slot ${slot.slot_id} (phase ${advancedLoop.current_phase}). `
|
|
2012
|
-
+ `Critique
|
|
2068
|
+
+ `Critique proposal artifact ${proposalArtifactId} and reply with evidence — do not edit code.`;
|
|
2013
2069
|
try {
|
|
2014
2070
|
const claimResult = createCoordinatorClaim({
|
|
2015
2071
|
agent: slot.agent,
|
|
@@ -2210,9 +2266,15 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
2210
2266
|
proposal_artifact_id: proposalArtifactId,
|
|
2211
2267
|
selected_targets: explicitTargets ? req.targetAgents : [],
|
|
2212
2268
|
mode: explicitTargets ? 'multi_agent' : 'single_agent',
|
|
2269
|
+
...(explicitTargets ? {
|
|
2270
|
+
ideation_schedule: req.ideation_schedule,
|
|
2271
|
+
pending_critics: Math.max(0, req.targetAgents.length - dispatchedCritics),
|
|
2272
|
+
} : {}),
|
|
2213
2273
|
dispatched_critics: dispatchedCritics,
|
|
2214
2274
|
current_phase: dispatchedPhase,
|
|
2215
|
-
|
|
2275
|
+
task_bytes: Buffer.byteLength(req.task, 'utf8'),
|
|
2276
|
+
task_truncated: false,
|
|
2277
|
+
delivery_plan: preparedCritics.map((p) => compactDeliveryEntry(p.entry)),
|
|
2216
2278
|
...(ideateExecStatus
|
|
2217
2279
|
? { execution_status: ideateExecStatus }
|
|
2218
2280
|
: explicitTargets
|