brainclaw 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/code-map.js +4 -1
- package/dist/commands/codev.js +61 -30
- package/dist/commands/doctor.js +14 -1
- package/dist/commands/harvest.js +196 -42
- package/dist/commands/inbox.js +10 -4
- package/dist/commands/loop.js +2 -2
- package/dist/commands/loops-handlers.js +82 -1
- package/dist/commands/mcp-catalog.js +12 -4
- package/dist/commands/mcp-read-handlers.js +90 -7
- package/dist/commands/mcp-schemas.generated.js +3 -0
- package/dist/commands/mcp-write-coordination.js +159 -40
- package/dist/commands/mcp.js +11 -2
- package/dist/core/agentrun-reconciler.js +171 -7
- package/dist/core/agentruns.js +6 -1
- package/dist/core/code-map/aggregate.js +473 -0
- package/dist/core/code-map/backend.js +36 -10
- package/dist/core/code-map/freshness.js +36 -1
- package/dist/core/code-map/lang/c/imports.scm +12 -0
- package/dist/core/code-map/lang/c/index.js +150 -0
- package/dist/core/code-map/lang/c/tags.scm +68 -0
- package/dist/core/code-map/lang/cpp/imports.scm +14 -0
- package/dist/core/code-map/lang/cpp/index.js +149 -0
- package/dist/core/code-map/lang/cpp/tags.scm +87 -0
- package/dist/core/code-map/lang/csharp/imports.scm +20 -0
- package/dist/core/code-map/lang/csharp/index.js +224 -0
- package/dist/core/code-map/lang/csharp/tags.scm +63 -0
- package/dist/core/code-map/lang/go/imports.scm +13 -0
- package/dist/core/code-map/lang/go/index.js +139 -0
- package/dist/core/code-map/lang/go/tags.scm +36 -0
- package/dist/core/code-map/lang/providers.js +12 -1
- package/dist/core/code-map/lang/ruby/imports.scm +24 -0
- package/dist/core/code-map/lang/ruby/index.js +198 -0
- package/dist/core/code-map/lang/ruby/tags.scm +49 -0
- package/dist/core/code-map/lang/rust/imports.scm +44 -0
- package/dist/core/code-map/lang/rust/index.js +136 -0
- package/dist/core/code-map/lang/rust/tags.scm +47 -0
- package/dist/core/code-map/query.js +229 -80
- package/dist/core/code-map/types.js +18 -0
- package/dist/core/code-map/work-section.js +8 -7
- package/dist/core/codev-responses.js +16 -0
- package/dist/core/dispatcher.js +176 -22
- package/dist/core/execution-adapters.js +29 -3
- package/dist/core/ideation-loop-close.js +124 -0
- package/dist/core/loops/artifact-resolver.js +197 -0
- package/dist/core/loops/attempt-reservation.js +576 -0
- package/dist/core/loops/commit-intent.js +494 -0
- package/dist/core/loops/facade-schema.js +48 -0
- package/dist/core/loops/impl-bind.js +144 -0
- package/dist/core/loops/index.js +1 -1
- package/dist/core/loops/iteration-engine.js +29 -0
- package/dist/core/loops/lock.js +14 -0
- package/dist/core/loops/project-resolution.js +157 -0
- package/dist/core/loops/reconcile-turn.js +369 -0
- package/dist/core/loops/result-reducers.js +88 -0
- package/dist/core/loops/store.js +46 -7
- package/dist/core/loops/types.js +139 -11
- package/dist/core/loops/verbs.js +9 -3
- package/dist/core/loops/verify-command.js +209 -0
- package/dist/core/messaging.js +58 -5
- package/dist/core/review-loop-close.js +5 -2
- package/dist/core/review-loop-turn-dispatch.js +290 -28
- package/dist/core/runtime-signals.js +68 -0
- package/dist/core/schema.js +24 -0
- package/dist/core/worktree.js +24 -0
- package/dist/facts.js +9 -9
- package/dist/facts.json +8 -8
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/docs/cli.md +1 -1
- package/docs/code-map.md +22 -6
- package/docs/concepts/loop-engine.md +24 -0
- package/docs/concepts/observer-protocol.md +22 -0
- package/docs/mcp-schema-changelog.md +43 -1
- package/package.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ZodError } from 'zod';
|
|
2
2
|
import { listAgentRuns } from '../core/agentruns.js';
|
|
3
3
|
import { reconcileAgentRun } from '../core/agentrun-reconciler.js';
|
|
4
|
+
import { findReservationByRunId } from '../core/loops/attempt-reservation.js';
|
|
5
|
+
import { runVerify } from '../core/loops/verify-command.js';
|
|
6
|
+
import { runImplBind } from '../core/loops/impl-bind.js';
|
|
4
7
|
import { add_artifact, advance, AwaitingFileApplyApprovalError, closeLoop, complete_turn, computeNextExpected, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, provideInput, requestInput, resume, sweepPauseTimeouts, turn, VersionConflictError, withLoopLock, } from '../core/loops/index.js';
|
|
5
8
|
import { BclawLoopRequestSchema, BCLAW_LOOP_INTENTS, } from '../core/loops/facade-schema.js';
|
|
6
9
|
// NextExpectedHint type now lives in src/core/loops/next-expected.ts
|
|
@@ -179,7 +182,7 @@ function trySweepLoopTimeouts(loop_id, cwd) {
|
|
|
179
182
|
}
|
|
180
183
|
catch { /* best-effort: never block facade on sweep errors */ }
|
|
181
184
|
}
|
|
182
|
-
export function handleBclawLoop(options) {
|
|
185
|
+
export async function handleBclawLoop(options) {
|
|
183
186
|
const startMs = Date.now();
|
|
184
187
|
const defaultActor = options.defaultActor ?? 'bclaw_loop';
|
|
185
188
|
const inferredIntent = inferIntent(options.args);
|
|
@@ -218,6 +221,7 @@ export function handleBclawLoop(options) {
|
|
|
218
221
|
linked: req.linked,
|
|
219
222
|
stop_condition: req.stop_condition,
|
|
220
223
|
mode: req.mode,
|
|
224
|
+
verify: req.verify,
|
|
221
225
|
created_by: agentId,
|
|
222
226
|
}, options.cwd);
|
|
223
227
|
const newEvents = findNewLoopEvents(loop.id, undefined, options.cwd);
|
|
@@ -257,7 +261,20 @@ export function handleBclawLoop(options) {
|
|
|
257
261
|
continue;
|
|
258
262
|
if (slotStatus === 'done' || slotStatus === 'failed' || slotStatus === 'cancelled')
|
|
259
263
|
continue;
|
|
264
|
+
// pln#630 PR2b-c (§13 R6): GET is strictly observational for
|
|
265
|
+
// TURN-OWNED slots. A slot carrying current_turn_id reconciles only
|
|
266
|
+
// via the dedicated mutating reconcile path (never on a read), so a
|
|
267
|
+
// stale/racing read can't phantom-complete its run. Legacy slots
|
|
268
|
+
// (no current_turn_id) keep the intentional lazy reconcile
|
|
269
|
+
// (trp_fdf3e590) that converges silent-completion on access.
|
|
270
|
+
if (slot.current_turn_id)
|
|
271
|
+
continue;
|
|
260
272
|
for (const run of listAgentRuns(options.cwd, { assignment_id: assignmentId })) {
|
|
273
|
+
// Belt-and-braces (review PR2b-c #D): skip by ACTUAL ownership too,
|
|
274
|
+
// not just the slot pointer — if a run is turn-owned but its slot
|
|
275
|
+
// wasn't stamped (write-ordering), GET must still not mutate it.
|
|
276
|
+
if (findReservationByRunId(run.id, options.cwd))
|
|
277
|
+
continue;
|
|
261
278
|
reconcileAgentRun(run.id, options.cwd);
|
|
262
279
|
}
|
|
263
280
|
}
|
|
@@ -426,6 +443,70 @@ export function handleBclawLoop(options) {
|
|
|
426
443
|
return successResponse('close', { loop, next_expected: null }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
427
444
|
});
|
|
428
445
|
}
|
|
446
|
+
case 'verify': {
|
|
447
|
+
// pln#632 — run the loop's opener-configured verify command + record a
|
|
448
|
+
// deterministic verify_report. runVerify manages its OWN two lock scopes (the
|
|
449
|
+
// spawn runs OUT of the lock), so it is NOT wrapped in withLockedLoopMutation.
|
|
450
|
+
const existing = getLoop(req.loop_id, options.cwd);
|
|
451
|
+
if (!existing) {
|
|
452
|
+
return errorResponse('verify', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
453
|
+
}
|
|
454
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
455
|
+
const result = runVerify({ loop_id: req.loop_id, actor }, options.cwd);
|
|
456
|
+
const newEvents = findNewLoopEvents(result.thread.id, beforeEvents, options.cwd);
|
|
457
|
+
const summary = result.unconfigured
|
|
458
|
+
? `verify: loop has no protocol.verify — falling back to an agent-narrated verify_report`
|
|
459
|
+
: result.deduped
|
|
460
|
+
? `verify: a verify_report already exists for iteration ${result.thread.iteration_count} (idempotent)`
|
|
461
|
+
: `${result.report?.passed ? '✔ verify green' : result.report?.timed_out ? '✖ verify RED (timeout)' : '✖ verify red'}: ${result.report?.command ?? ''}`;
|
|
462
|
+
return successResponse('verify', {
|
|
463
|
+
loop: result.thread,
|
|
464
|
+
verify_report: result.report ?? null,
|
|
465
|
+
deduped: result.deduped,
|
|
466
|
+
unconfigured: result.unconfigured ?? false,
|
|
467
|
+
next_expected: computeNextExpected(result.thread),
|
|
468
|
+
}, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summary);
|
|
469
|
+
}
|
|
470
|
+
case 'bind': {
|
|
471
|
+
// pln#632 impl-loop bind — dispatch the loop's linked sequence + advance
|
|
472
|
+
// bind→execute. runImplBind awaits the async spawn (the advance takes its own
|
|
473
|
+
// lock via the verb), so it is NOT wrapped in withLockedLoopMutation — it mirrors
|
|
474
|
+
// coordinate(open_loop)'s async-handler-spawns pattern, not a synchronous verb.
|
|
475
|
+
const existing = getLoop(req.loop_id, options.cwd);
|
|
476
|
+
if (!existing) {
|
|
477
|
+
return errorResponse('bind', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
478
|
+
}
|
|
479
|
+
if (existing.kind !== 'implementation') {
|
|
480
|
+
return errorResponse('bind', 'validation_error', `bind is only valid for implementation loops (loop ${req.loop_id} is kind='${existing.kind}'); review/ideation loops dispatch via bclaw_coordinate`, Date.now() - startMs);
|
|
481
|
+
}
|
|
482
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
483
|
+
const bind = await runImplBind({
|
|
484
|
+
loop_id: req.loop_id,
|
|
485
|
+
dispatcherAgent: actor,
|
|
486
|
+
dispatcherAgentId: agentId,
|
|
487
|
+
sessionId: options.sessionId,
|
|
488
|
+
dryRun: req.dry_run,
|
|
489
|
+
lanes: req.lanes,
|
|
490
|
+
autoExecute: req.auto_execute,
|
|
491
|
+
model: req.model,
|
|
492
|
+
maxAssignments: req.max_assignments,
|
|
493
|
+
}, options.cwd);
|
|
494
|
+
const loop = getLoop(req.loop_id, options.cwd);
|
|
495
|
+
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
496
|
+
const sideEffects = bind.action === 'bound'
|
|
497
|
+
? [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)]
|
|
498
|
+
: [...loopEventSideEffects(newEvents)];
|
|
499
|
+
return successResponse('bind', {
|
|
500
|
+
loop,
|
|
501
|
+
sequence_id: bind.sequence_id,
|
|
502
|
+
action: bind.action,
|
|
503
|
+
advanced_to: bind.advanced_to ?? null,
|
|
504
|
+
auto_closed: bind.auto_closed ?? false,
|
|
505
|
+
dispatched: bind.messages_sent,
|
|
506
|
+
dispatch: bind.dispatch,
|
|
507
|
+
next_expected: computeNextExpected(loop),
|
|
508
|
+
}, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], sideEffects, bind.dispatch?.warnings ?? [], Date.now() - startMs, bind.reason);
|
|
509
|
+
}
|
|
429
510
|
}
|
|
430
511
|
}
|
|
431
512
|
catch (err) {
|
|
@@ -360,19 +360,22 @@ export const MCP_READ_TOOLS = [
|
|
|
360
360
|
},
|
|
361
361
|
{
|
|
362
362
|
name: 'bclaw_read_inbox',
|
|
363
|
-
description: 'Read messages from an agent inbox.
|
|
363
|
+
description: 'Read messages from an agent inbox, newest-first. By default returns only ACTIONABLE messages (pending + read) and hides acknowledged/archived — pass includeAll=true, or a specific status, to widen. Message bodies are previewed (~500 chars) unless full=true; the page is size-bounded by budget_tokens so a read can never blow the token budget. Use markAsRead to auto-mark pending messages as read.',
|
|
364
364
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
365
365
|
inputSchema: {
|
|
366
366
|
type: 'object',
|
|
367
367
|
properties: {
|
|
368
368
|
agent: { type: 'string', description: 'Agent name whose inbox to read. Defaults to calling agent.' },
|
|
369
369
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
370
|
-
status: { type: 'string', description: 'Filter by status: pending, read, acknowledged, archived.' },
|
|
370
|
+
status: { type: 'string', description: 'Filter by an exact status: pending, read, acknowledged, archived. Overrides the actionable default.' },
|
|
371
|
+
includeAll: { type: 'boolean', description: 'Return every status (including acknowledged + archived), disabling the actionable default. Default: false.' },
|
|
371
372
|
type: { type: 'string', description: 'Filter by message type: assign, review, rfc, info, reply.' },
|
|
372
373
|
thread_id: { type: 'string', description: 'Filter by thread ID to see a conversation.' },
|
|
374
|
+
full: { type: 'boolean', description: 'Return complete message bodies instead of ~500-char previews. Each previewed message reports text_length + truncated:true. Default: false.' },
|
|
373
375
|
markAsRead: { type: 'boolean', description: 'Mark pending messages as read. Default: false.' },
|
|
374
376
|
limit: { type: 'number', description: 'Maximum messages to return (default: 20).' },
|
|
375
377
|
offset: { type: 'number', description: 'Skip N messages for pagination.' },
|
|
378
|
+
budget_tokens: { type: 'number', description: 'Cap the returned page size (~4 chars/token). Messages are trimmed until the payload fits; has_more/next_offset let you page the rest.' },
|
|
376
379
|
},
|
|
377
380
|
},
|
|
378
381
|
},
|
|
@@ -1028,8 +1031,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
1028
1031
|
// created a loop structure without dispatching the first turn, so
|
|
1029
1032
|
// nothing ever ran. Loops are opened via
|
|
1030
1033
|
// bclaw_coordinate(intent='review', open_loop=true) or intent='ideate'.
|
|
1031
|
-
enum: ['get', 'list', 'turn', 'complete_turn', 'advance', 'add_artifact', 'pause', 'resume', 'close'],
|
|
1032
|
-
description: 'Loop lifecycle intent for driving turns inside a loop that was already opened via the coordinate facade. To START a loop, use `bclaw_coordinate(intent="review", open_loop=true, targetAgents=[…])` or `intent="ideate"` — that opens the loop AND dispatches the first turn. See docs/concepts/loop-engine.md.',
|
|
1034
|
+
enum: ['get', 'list', 'turn', 'complete_turn', 'advance', 'add_artifact', 'pause', 'resume', 'close', 'bind'],
|
|
1035
|
+
description: 'Loop lifecycle intent for driving turns inside a loop that was already opened via the coordinate facade. To START a loop, use `bclaw_coordinate(intent="review", open_loop=true, targetAgents=[…])` or `intent="ideate"` — that opens the loop AND dispatches the first turn. `bind` (implementation loops only) dispatches the loop\'s linked sequence and advances bind→execute — the engine action for the `bind` phase. See docs/concepts/loop-engine.md.',
|
|
1033
1036
|
},
|
|
1034
1037
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
1035
1038
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -1052,6 +1055,11 @@ const MCP_WRITE_TOOLS = [
|
|
|
1052
1055
|
outcome: { type: 'string', enum: ['done', 'failed', 'cancelled'], description: 'complete_turn outcome (default done).' },
|
|
1053
1056
|
failure_reason: { type: 'string', description: 'complete_turn: optional failure/cancel reason.' },
|
|
1054
1057
|
artifact: { type: 'object', description: 'complete_turn / add_artifact payload: { phase, type, body?, produced_by?, ref? }.' },
|
|
1058
|
+
dry_run: { type: 'boolean', description: 'bind: analyze + report what would dispatch; no spawn, no advance.' },
|
|
1059
|
+
lanes: { type: 'array', items: { type: 'string' }, description: 'bind: restrict the dispatch to specific sequence lanes.' },
|
|
1060
|
+
auto_execute: { type: 'boolean', description: 'bind: deliver briefs without spawning (→ manual launch commands).' },
|
|
1061
|
+
model: { type: 'string', description: 'bind: model override for the dispatched agents.' },
|
|
1062
|
+
max_assignments: { type: 'number', description: 'bind: cap assignments made in this bind.' },
|
|
1055
1063
|
to_phase: { type: 'string', description: 'advance: explicit target phase (otherwise the next phase).' },
|
|
1056
1064
|
force: { type: 'boolean', description: 'advance: allow going backwards (increments iteration_count).' },
|
|
1057
1065
|
reason: { type: 'string', description: 'advance / pause / close: optional reason string.' },
|
|
@@ -149,6 +149,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
149
149
|
let agentNameCache;
|
|
150
150
|
return dispatchReadTool(name, args, {
|
|
151
151
|
cwd,
|
|
152
|
+
sourceCwd: effective.cwd,
|
|
152
153
|
activeSource,
|
|
153
154
|
resolvedProject,
|
|
154
155
|
projectRoutingApplied,
|
|
@@ -1803,6 +1804,8 @@ function dispatchReadTool(name, args, ctx) {
|
|
|
1803
1804
|
if (name === 'bclaw_read_inbox') {
|
|
1804
1805
|
const agentName = args.agent ?? ctx.getAgentName();
|
|
1805
1806
|
const markAsRead = args.markAsRead === true; // default: false — reading doesn't imply processing
|
|
1807
|
+
const includeAll = args.includeAll === true; // pln#627 Phase A — widen past the actionable default
|
|
1808
|
+
const full = args.full === true; // pln#627 Phase A — return whole bodies, not previews
|
|
1806
1809
|
const result = readInbox({
|
|
1807
1810
|
agent: agentName,
|
|
1808
1811
|
status: args.status,
|
|
@@ -1810,21 +1813,77 @@ function dispatchReadTool(name, args, ctx) {
|
|
|
1810
1813
|
thread_id: args.thread_id,
|
|
1811
1814
|
limit: args.limit,
|
|
1812
1815
|
offset: args.offset,
|
|
1816
|
+
includeAll,
|
|
1813
1817
|
markAsRead,
|
|
1814
1818
|
}, cwd);
|
|
1815
|
-
|
|
1816
|
-
|
|
1819
|
+
// pln#627 Phase A — bound the payload so a single inbox read can never blow
|
|
1820
|
+
// the MCP token budget (root cause: persona/CoDev dumps persisted as inbox
|
|
1821
|
+
// messages, one at 960 KB). Two independent guards:
|
|
1822
|
+
// 1. per-message: preview each body to INBOX_PREVIEW_CHARS unless full=true;
|
|
1823
|
+
// the whole body stays available via full=true (bclaw_get(inbox_message)
|
|
1824
|
+
// is per-agent-scoped and cannot serve one message by id).
|
|
1825
|
+
// 2. whole-page: boundListResult trims messages until the JSON fits the
|
|
1826
|
+
// char budget, the same way bclaw_find / bclaw_search do (~4 chars/token).
|
|
1827
|
+
const INBOX_PREVIEW_CHARS = 500;
|
|
1828
|
+
const projected = result.messages.map((msg) => {
|
|
1829
|
+
const textLength = msg.text.length;
|
|
1830
|
+
const truncated = !full && textLength > INBOX_PREVIEW_CHARS;
|
|
1831
|
+
return {
|
|
1832
|
+
...msg,
|
|
1833
|
+
text: truncated ? msg.text.slice(0, INBOX_PREVIEW_CHARS) : msg.text,
|
|
1834
|
+
text_length: textLength,
|
|
1835
|
+
truncated,
|
|
1836
|
+
};
|
|
1837
|
+
});
|
|
1838
|
+
const budgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
1839
|
+
const charBudget = budgetTokens ? Math.min(budgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
|
|
1840
|
+
const bounded = boundListResult({ entity: 'inbox_message', total: result.total, items: projected }, result.offset, charBudget);
|
|
1841
|
+
const scopeNote = includeAll || args.status ? '' : ' actionable (pending+read); pass includeAll=true for acknowledged/archived';
|
|
1842
|
+
const lines = [`Inbox for ${agentName} — ${result.total} message(s)${scopeNote}:`];
|
|
1843
|
+
for (const msg of bounded.items) {
|
|
1817
1844
|
const ack = msg.requires_ack ? ' [ACK required]' : '';
|
|
1818
1845
|
const thread = msg.thread_id ? ` thread:${msg.thread_id}` : '';
|
|
1819
1846
|
lines.push(` [${msg.short_label ?? msg.id}] ${msg.type} from ${msg.from} (${msg.status})${ack}${thread}`);
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1847
|
+
const preview = msg.text.slice(0, 200);
|
|
1848
|
+
const more = msg.truncated || msg.text_length > 200;
|
|
1849
|
+
const moreNote = more
|
|
1850
|
+
? `… (${msg.text_length} chars${msg.truncated ? '; pass full=true for the whole body' : ''})`
|
|
1851
|
+
: '';
|
|
1852
|
+
lines.push(` ${preview}${moreNote}`);
|
|
1853
|
+
}
|
|
1854
|
+
if (bounded.items.length === 0) {
|
|
1823
1855
|
lines.push(' (no messages)');
|
|
1824
1856
|
}
|
|
1857
|
+
if (bounded.hint)
|
|
1858
|
+
lines.push('', bounded.hint);
|
|
1859
|
+
const nextActions = bounded.has_more
|
|
1860
|
+
? [{
|
|
1861
|
+
tool: 'bclaw_read_inbox',
|
|
1862
|
+
args: {
|
|
1863
|
+
...(args.agent ? { agent: args.agent } : {}),
|
|
1864
|
+
offset: bounded.next_offset,
|
|
1865
|
+
...(args.limit ? { limit: args.limit } : {}),
|
|
1866
|
+
...(args.status ? { status: args.status } : {}),
|
|
1867
|
+
...(includeAll ? { includeAll: true } : {}),
|
|
1868
|
+
},
|
|
1869
|
+
when: 'to fetch the next page',
|
|
1870
|
+
}]
|
|
1871
|
+
: [];
|
|
1825
1872
|
return {
|
|
1826
1873
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
1827
|
-
structuredContent: {
|
|
1874
|
+
structuredContent: {
|
|
1875
|
+
total: result.total,
|
|
1876
|
+
offset: result.offset,
|
|
1877
|
+
limit: result.limit,
|
|
1878
|
+
messages: bounded.items,
|
|
1879
|
+
returned: bounded.returned,
|
|
1880
|
+
has_more: bounded.has_more,
|
|
1881
|
+
...(bounded.next_offset !== undefined ? { next_offset: bounded.next_offset } : {}),
|
|
1882
|
+
...(bounded.omitted_for_size ? { omitted_for_size: bounded.omitted_for_size } : {}),
|
|
1883
|
+
...(bounded.hint ? { hint: bounded.hint } : {}),
|
|
1884
|
+
...(nextActions.length ? { next_actions: nextActions } : {}),
|
|
1885
|
+
schema_version: SCHEMA_VERSION,
|
|
1886
|
+
},
|
|
1828
1887
|
};
|
|
1829
1888
|
}
|
|
1830
1889
|
if (name === 'bclaw_context') {
|
|
@@ -1915,9 +1974,33 @@ function dispatchReadTool(name, args, ctx) {
|
|
|
1915
1974
|
` stderr: ${status.runtime.log_files.stderr?.exists ? `${status.runtime.log_files.stderr.size_bytes}B` : 'absent'}`,
|
|
1916
1975
|
` git: commits_ahead=${status.runtime.commits_ahead ?? 'n/a'} dirty_tracked=${status.runtime.dirty_tracked ?? 'n/a'}`,
|
|
1917
1976
|
];
|
|
1977
|
+
// pln#521 P1 (B4) — routing echo. Operators debugging a dispatch need to see
|
|
1978
|
+
// which project this status was read from, and WHY that project won, without
|
|
1979
|
+
// reverse-engineering cwd + store state. The decision (project_name/
|
|
1980
|
+
// project_cwd) is a first-class field; the reasoning is the `_resolution_trace`
|
|
1981
|
+
// sibling, which by design ships here and nowhere else. Deliberately cheap:
|
|
1982
|
+
// no candidate/nested-store scan on a hot read path. source_cwd is the
|
|
1983
|
+
// PRE-routing cwd (ctx.sourceCwd) — using the routed cwd would make the two
|
|
1984
|
+
// ends identical exactly when a `project` arg hopped stores, i.e. the one
|
|
1985
|
+
// case the trace exists to show.
|
|
1986
|
+
const projectCwd = resolvedProject?.path ?? cwd;
|
|
1987
|
+
const projectName = resolvedProject?.name;
|
|
1988
|
+
const resolutionTrace = {
|
|
1989
|
+
source_cwd: ctx.sourceCwd,
|
|
1990
|
+
effective_cwd: projectCwd,
|
|
1991
|
+
active_source: activeSource,
|
|
1992
|
+
...(projectRoutingApplied && typeof args.project === 'string' ? { project_arg: args.project } : {}),
|
|
1993
|
+
};
|
|
1994
|
+
lines.push('', `Project: ${projectName ?? '(unnamed)'} — ${projectCwd} (via ${activeSource})`);
|
|
1918
1995
|
return {
|
|
1919
1996
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
1920
|
-
structuredContent: {
|
|
1997
|
+
structuredContent: {
|
|
1998
|
+
...status,
|
|
1999
|
+
project_cwd: projectCwd,
|
|
2000
|
+
...(projectName ? { project_name: projectName } : {}),
|
|
2001
|
+
_resolution_trace: resolutionTrace,
|
|
2002
|
+
schema_version: SCHEMA_VERSION,
|
|
2003
|
+
},
|
|
1921
2004
|
};
|
|
1922
2005
|
}
|
|
1923
2006
|
throw new Error(`Unknown read tool: ${name}`);
|
|
@@ -20,12 +20,14 @@ import { appendAuditEntry } from '../core/audit.js';
|
|
|
20
20
|
import { nowISO } from '../core/ids.js';
|
|
21
21
|
import { validateMcpField } from '../core/input-validation.js';
|
|
22
22
|
import { generateCandidateIdWithLabel, saveCandidate } from '../core/candidates.js';
|
|
23
|
+
import { validateLoopProjectResolution } from '../core/loops/project-resolution.js';
|
|
23
24
|
import { ackMessage, getThread, hasActiveAssignment, sendMessage } from '../core/messaging.js';
|
|
24
25
|
import { dispatch, dispatchReview, generateDispatchBrief } from '../core/dispatcher.js';
|
|
25
26
|
import { CoordinateRequestSchema } from '../core/facade-schema.js';
|
|
26
27
|
import { buildInvokeCommand, getCapabilityProfile, getSpawnableAgents, resolveModel, validateAgentForDispatch, } from '../core/agent-capability.js';
|
|
27
28
|
import { attemptExecution } from '../core/execution.js';
|
|
28
29
|
import { createAgentRun, transitionAgentRun } from '../core/agentruns.js';
|
|
30
|
+
import { prepareTurnOwnedReviewDispatch, turnOwnedReviewEnabled } from '../core/review-loop-turn-dispatch.js';
|
|
29
31
|
import { createAssignment, generateAssignmentId, patchAssignmentMessageId, transitionAssignment, } from '../core/assignments.js';
|
|
30
32
|
import { createToolErrorResponse, toolResponse, } from './mcp-contract.js';
|
|
31
33
|
import { handleMcpReadToolCall } from './mcp-read-handlers.js';
|
|
@@ -228,11 +230,13 @@ export function handleBclawSendMessage(args, ctx) {
|
|
|
228
230
|
}, cwd);
|
|
229
231
|
appendAuditEntry({ actor: resolved.identity.agent_name, actor_id: resolved.identity.agent_id, action: 'create', item_id: result.id, item_type: 'message', scope: to }, cwd);
|
|
230
232
|
const threadInfo = threadId ? ` thread:${threadId}` : '';
|
|
233
|
+
const warningLine = result.warning ? `\n⚠ ${result.warning}` : '';
|
|
231
234
|
return {
|
|
232
235
|
response: toolResponse({
|
|
233
|
-
content: [{ type: 'text', text: `✔ Message sent: [${result.shortLabel}] ${msgType} → ${to}${threadInfo}` }],
|
|
236
|
+
content: [{ type: 'text', text: `✔ Message sent: [${result.shortLabel}] ${msgType} → ${to}${threadInfo}${warningLine}` }],
|
|
234
237
|
message_id: result.id,
|
|
235
238
|
thread_id: threadId,
|
|
239
|
+
...(result.warning ? { warning: result.warning } : {}),
|
|
236
240
|
}),
|
|
237
241
|
};
|
|
238
242
|
}
|
|
@@ -270,7 +274,7 @@ export function handleBclawAckMessage(args, ctx) {
|
|
|
270
274
|
}
|
|
271
275
|
}
|
|
272
276
|
export async function handleBclawCoordinate(args, ctx) {
|
|
273
|
-
const { cwd, connectionSessionId, currentModel } = ctx;
|
|
277
|
+
const { cwd, connectionSessionId, currentModel, effectiveScope } = ctx;
|
|
274
278
|
const startMs = Date.now();
|
|
275
279
|
const parseResult = CoordinateRequestSchema.safeParse(args);
|
|
276
280
|
if (!parseResult.success) {
|
|
@@ -356,6 +360,35 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
356
360
|
warnings.push(`cross-project dispatch (project='${req.project}') — auto-spawn disabled; the target agent picks up the brief async via its own bclaw_work.`);
|
|
357
361
|
}
|
|
358
362
|
const effectiveAutoExecute = isCrossProject ? false : req.autoExecute;
|
|
363
|
+
// pln#521 P1 — project resolution gate. A review loop written into the wrong
|
|
364
|
+
// store is worse than one that never opened: candidate, claim, assignment and
|
|
365
|
+
// loop all persist where nobody is watching, and the reviewer spawns against
|
|
366
|
+
// the wrong repo (DGX misroute). So when this store can host several projects
|
|
367
|
+
// and NONE was selected, refuse here — before the pre-flight spawn and before
|
|
368
|
+
// the first write — instead of silently defaulting to cwd. Explicit `project`,
|
|
369
|
+
// a session switch, or an active-project pointer all count as a choice; ref /
|
|
370
|
+
// scope / path never do (B3 rejected, art_e29e88878209). Scoped to the failing
|
|
371
|
+
// path (review + open_loop): every other intent keeps its routing untouched.
|
|
372
|
+
let projectResolution;
|
|
373
|
+
if (req.intent === 'review' && req.open_loop === true) {
|
|
374
|
+
// Resolve from `cwd`, not `dispatchCwd`: an explicit project name is
|
|
375
|
+
// resolvable from the SOURCE store (its links / store chain), which is
|
|
376
|
+
// exactly what produced dispatchCwd above. Re-resolving from the target
|
|
377
|
+
// could fail for a link whose name differs from the target's project_name.
|
|
378
|
+
const resolution = validateLoopProjectResolution({
|
|
379
|
+
cwd,
|
|
380
|
+
projectArg: req.project,
|
|
381
|
+
activeSource: effectiveScope?.active_source,
|
|
382
|
+
});
|
|
383
|
+
if (!resolution.ok) {
|
|
384
|
+
return {
|
|
385
|
+
response: createToolErrorResponse(resolution.code, resolution.message, {
|
|
386
|
+
candidates: resolution.candidates,
|
|
387
|
+
}),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
projectResolution = resolution;
|
|
391
|
+
}
|
|
359
392
|
// can_30c295b4 / trp#371 Tier 2 — scope-aware dirty-working-tree guard.
|
|
360
393
|
// Intents that spawn a worktree worker from HEAD can review/edit stale code,
|
|
361
394
|
// so they are guarded; consult/summarize (no worktree) are not. pln#626
|
|
@@ -403,7 +436,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
403
436
|
/** Run E2E execution phase on prepared delivery entries. Returns overall execution status. */
|
|
404
437
|
const runCoordinateExecution = async (prepared, opts) => {
|
|
405
438
|
let overall = 'inbox_only';
|
|
406
|
-
for (const { entry, invoke, worktreePath } of prepared) {
|
|
439
|
+
for (const { entry, invoke, worktreePath, turnEcho } of prepared) {
|
|
407
440
|
const execResult = await attemptExecution(invoke, {
|
|
408
441
|
agent: entry.agent,
|
|
409
442
|
autoExecute: opts.autoExecute,
|
|
@@ -414,6 +447,8 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
414
447
|
dispatcherAgentId: opts.senderAgentId,
|
|
415
448
|
cwd: opts.cwd,
|
|
416
449
|
requireWorktree: true, // pln#531: never spawn a worker in the integration repo
|
|
450
|
+
turnEcho, // pln#630 — turn-owned reviewer (initial dispatch): the ack-wrapper writes the
|
|
451
|
+
// turn-keyed completion sentinel. undefined for every non-turn-owned entry.
|
|
417
452
|
});
|
|
418
453
|
entry.execution_status = execResult.execution_status;
|
|
419
454
|
// pln#626 Phase 1 — carry the machine-readable reason (+ failure_kind) to
|
|
@@ -436,7 +471,23 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
436
471
|
// emit three identical context-free warnings (pln#626 Phase 1 R5).
|
|
437
472
|
if (execResult.error)
|
|
438
473
|
opts.warnings.push(`${entry.agent}: ${execResult.error}`);
|
|
439
|
-
if (
|
|
474
|
+
if (turnEcho) {
|
|
475
|
+
// pln#630 risk #1 — a turn-owned reviewer's run was ALREADY created (`created`) by
|
|
476
|
+
// prepareTurnOwnedReviewDispatch. Do NOT mint a second run here (double-mint). Transition
|
|
477
|
+
// the deterministic run → running on a real spawn (mirrors dispatchReviewLoopTurn); leave
|
|
478
|
+
// it `created` otherwise so the no-sentinel legacy fallback (turnOwnedLaneEvidence) + the
|
|
479
|
+
// pre-run lease reconciler govern it. Non-turn-owned entries take the unchanged else-branch.
|
|
480
|
+
if (execResult.execution_status === 'delivered_and_started') {
|
|
481
|
+
try {
|
|
482
|
+
transitionAgentRun(turnEcho.run_id, 'running', {
|
|
483
|
+
actor: opts.senderAgent, actor_id: opts.senderAgentId, pid: execResult.pid,
|
|
484
|
+
status_reason: 'turn-owned reviewer spawned by coordinator',
|
|
485
|
+
}, opts.cwd);
|
|
486
|
+
}
|
|
487
|
+
catch { /* best-effort — the reconciler converges if this races */ }
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (entry.assignment_id && entry.claim_id) {
|
|
440
491
|
if (execResult.failure_kind === 'spawn_no_handshake') {
|
|
441
492
|
try {
|
|
442
493
|
const run = createAgentRun({
|
|
@@ -969,38 +1020,82 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
969
1020
|
id: claimResult.claimId,
|
|
970
1021
|
});
|
|
971
1022
|
let reviewAssignmentId;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1023
|
+
let reviewTurnEcho;
|
|
1024
|
+
// pln#630 — turn-own the INITIAL reviewer dispatch (same default + kill-switch as the
|
|
1025
|
+
// fix cycle). Skipped for cross-project reviews (no local worktree/sentinel → they never
|
|
1026
|
+
// spawn here). WON: prepare minted the DETERMINISTIC assignment + run + turn()-bound the
|
|
1027
|
+
// slot, so we reuse prep.assignmentId for the brief/message/linkage below and carry the
|
|
1028
|
+
// turnEcho so the ack-wrapper writes the turn-keyed sentinel. DENIED: the exactly-once
|
|
1029
|
+
// fence says this dispatch is NOT the spawner — do NOT spawn, do NOT fall back to legacy
|
|
1030
|
+
// (that is the double-spawn hole), and do NOT release the (possibly shared) claim; leave
|
|
1031
|
+
// the slot for reconcile/self-heal. LEGACY: the unchanged inline mint runs.
|
|
1032
|
+
let usedTurnOwned = false;
|
|
1033
|
+
if (turnOwnedReviewEnabled() && !req.project) {
|
|
1034
|
+
const prep = prepareTurnOwnedReviewDispatch({
|
|
1035
|
+
loopId: loop.id,
|
|
1036
|
+
slotId: slot.slot_id,
|
|
978
1037
|
agent: slot.agent ?? '',
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1038
|
+
agentId: slot.agent_id,
|
|
1039
|
+
phase: 'findings',
|
|
1040
|
+
task: req.task,
|
|
982
1041
|
description: reviewDescription,
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1042
|
+
scope: reviewScope,
|
|
1043
|
+
claimId: claimResult.claimId,
|
|
1044
|
+
worktreePath: claimResult.worktreePath,
|
|
1045
|
+
dispatcherAgent: senderAgent,
|
|
1046
|
+
dispatcherAgentId: senderAgentId,
|
|
1047
|
+
sessionId: connectionSessionId,
|
|
1048
|
+
isReviewer: true,
|
|
1049
|
+
cwd: dispatchCwd,
|
|
1050
|
+
});
|
|
1051
|
+
if (prep.kind === 'won') {
|
|
1052
|
+
reviewAssignmentId = prep.assignmentId; // deterministic — harvest correlates on it
|
|
1053
|
+
reviewTurnEcho = { turn_id: prep.turnId, run_id: prep.runId, nonce: prep.nonce };
|
|
1054
|
+
out.artifacts.push({ type: 'assignment', id: prep.assignmentId });
|
|
1055
|
+
usedTurnOwned = true; // prepare already created the assignment + run + bound the slot
|
|
1056
|
+
}
|
|
1057
|
+
else if (prep.kind === 'denied') {
|
|
1058
|
+
out.partial = true;
|
|
1059
|
+
out.warnings.push(`open_loop: turn-owned reviewer dispatch denied for slot ${slot.slot_id} (${prep.reason}); not spawning — slot left for reconcile/self-heal`);
|
|
1060
|
+
continue; // MUST NOT spawn AND MUST NOT legacy-fallback beside a live reservation
|
|
1061
|
+
}
|
|
1062
|
+
// prep.kind === 'legacy' (pre-identity failure) → fall through to the inline mint.
|
|
987
1063
|
}
|
|
988
|
-
|
|
989
|
-
|
|
1064
|
+
if (!usedTurnOwned) {
|
|
1065
|
+
try {
|
|
1066
|
+
const preId = generateAssignmentId(dispatchCwd);
|
|
1067
|
+
const assignment = createAssignment({
|
|
1068
|
+
id: preId.id,
|
|
1069
|
+
short_label: preId.short_label,
|
|
1070
|
+
claim_id: claimResult.claimId,
|
|
1071
|
+
agent: slot.agent ?? '',
|
|
1072
|
+
dispatcher_agent: senderAgent,
|
|
1073
|
+
dispatcher_session_id: connectionSessionId,
|
|
1074
|
+
scope: reviewScope,
|
|
1075
|
+
description: reviewDescription,
|
|
1076
|
+
tags: ['coordinate', 'review', 'loop'],
|
|
1077
|
+
}, dispatchCwd);
|
|
1078
|
+
reviewAssignmentId = assignment.id;
|
|
1079
|
+
out.artifacts.push({ type: 'assignment', id: assignment.id });
|
|
1080
|
+
}
|
|
1081
|
+
catch (asgErr) {
|
|
1082
|
+
out.warnings.push(`Review assignment creation failed for slot ${slot.slot_id}: ${asgErr instanceof Error ? asgErr.message : String(asgErr)}`);
|
|
1083
|
+
}
|
|
1084
|
+
// pln#628 Focus 4B (BLOCKING 2) — assign the slot NOW that the
|
|
1085
|
+
// claim/assignment exist, binding their ids onto the slot so the
|
|
1086
|
+
// harvest close resolves this exact reviewer by assignment_id. Runs
|
|
1087
|
+
// even if assignment creation failed (undefined id → the harvest
|
|
1088
|
+
// falls back to the legacy agent match for this one slot). (For the
|
|
1089
|
+
// turn-owned WON path, prepare already turn()-bound the slot.)
|
|
1090
|
+
turn({
|
|
1091
|
+
id: loop.id,
|
|
1092
|
+
slot_id: slot.slot_id,
|
|
1093
|
+
actor: creatorActor,
|
|
1094
|
+
input: req.task,
|
|
1095
|
+
assignment_id: reviewAssignmentId,
|
|
1096
|
+
claim_id: claimResult.claimId,
|
|
1097
|
+
}, dispatchCwd);
|
|
990
1098
|
}
|
|
991
|
-
// pln#628 Focus 4B (BLOCKING 2) — assign the slot NOW that the
|
|
992
|
-
// claim/assignment exist, binding their ids onto the slot so the
|
|
993
|
-
// harvest close resolves this exact reviewer by assignment_id. Runs
|
|
994
|
-
// even if assignment creation failed (undefined id → the harvest
|
|
995
|
-
// falls back to the legacy agent match for this one slot).
|
|
996
|
-
turn({
|
|
997
|
-
id: loop.id,
|
|
998
|
-
slot_id: slot.slot_id,
|
|
999
|
-
actor: creatorActor,
|
|
1000
|
-
input: req.task,
|
|
1001
|
-
assignment_id: reviewAssignmentId,
|
|
1002
|
-
claim_id: claimResult.claimId,
|
|
1003
|
-
}, dispatchCwd);
|
|
1004
1099
|
const reviewBrief = buildCoordinateBrief(slot.agent ?? '', reviewDescription + reviewVerdictBriefSuffix, {
|
|
1005
1100
|
claimId: claimResult.claimId,
|
|
1006
1101
|
scope: reviewScope,
|
|
@@ -1045,6 +1140,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1045
1140
|
entry: queued.entry,
|
|
1046
1141
|
invoke: queued.invoke,
|
|
1047
1142
|
worktreePath: claimResult.worktreePath,
|
|
1143
|
+
turnEcho: reviewTurnEcho,
|
|
1048
1144
|
});
|
|
1049
1145
|
}
|
|
1050
1146
|
catch (dispatchErr) {
|
|
@@ -1105,6 +1201,16 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1105
1201
|
result = {
|
|
1106
1202
|
candidate_id: output.candidateId,
|
|
1107
1203
|
selected_targets: resolvedAgents,
|
|
1204
|
+
// pln#521 P1 (B4) — echo the routing decision so an operator can see WHERE
|
|
1205
|
+
// the loop landed without reverse-engineering cwd and store state. Present
|
|
1206
|
+
// for open_loop reviews (the gated path); the reasoning behind the decision
|
|
1207
|
+
// ships as `_resolution_trace` on dispatch_status, not here.
|
|
1208
|
+
...(projectResolution
|
|
1209
|
+
? {
|
|
1210
|
+
project_cwd: projectResolution.project_cwd,
|
|
1211
|
+
...(projectResolution.project_name ? { project_name: projectResolution.project_name } : {}),
|
|
1212
|
+
}
|
|
1213
|
+
: {}),
|
|
1108
1214
|
// pln#626 Phase 1 (review rework) — expose the reviewer delivery entries
|
|
1109
1215
|
// so review is as honest as assign/reroute: each entry's execution_reason
|
|
1110
1216
|
// (set by runCoordinateExecution) feeds the top-level derivation, so a
|
|
@@ -1550,12 +1656,6 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1550
1656
|
slotRole: slot.role,
|
|
1551
1657
|
memoryProvider: provider,
|
|
1552
1658
|
});
|
|
1553
|
-
turn({
|
|
1554
|
-
id: loopId,
|
|
1555
|
-
slot_id: slot.slot_id,
|
|
1556
|
-
actor: creatorActor,
|
|
1557
|
-
input: briefResult.text,
|
|
1558
|
-
}, dispatchCwd);
|
|
1559
1659
|
// pln#626 Phase 2 (Option B) — spawn the critic as a worktree-isolated
|
|
1560
1660
|
// worker, mirroring the intent=assign / review chain. Each critic gets
|
|
1561
1661
|
// its OWN claim + worktree (scope is unique per slot) so parallel
|
|
@@ -1605,6 +1705,23 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1605
1705
|
catch (asgErr) {
|
|
1606
1706
|
warnings.push(`ideate assignment creation failed for slot ${slot.slot_id}: ${asgErr instanceof Error ? asgErr.message : String(asgErr)}`);
|
|
1607
1707
|
}
|
|
1708
|
+
// pln#629 — bind the slot to its claim/assignment NOW that both
|
|
1709
|
+
// exist (mirrors the review path, pln#628 BLOCKING 2). The turn()
|
|
1710
|
+
// used to fire BEFORE the assignment was created, leaving
|
|
1711
|
+
// slot.assignment_id undefined: bclaw_loop get's reconcile then
|
|
1712
|
+
// skipped the critic slot (loops-handlers.ts `if (!assignmentId)
|
|
1713
|
+
// continue`) and dispatch_status(lop_) resolved no assignment, so
|
|
1714
|
+
// ideate loops could never be reconciled (trp_dfe0b941 /
|
|
1715
|
+
// trp_2187b340 / trp_1de94516). Runs even if assignment creation
|
|
1716
|
+
// failed (undefined id → legacy agent-match fallback, as review).
|
|
1717
|
+
turn({
|
|
1718
|
+
id: loopId,
|
|
1719
|
+
slot_id: slot.slot_id,
|
|
1720
|
+
actor: creatorActor,
|
|
1721
|
+
input: briefResult.text,
|
|
1722
|
+
assignment_id: criticAssignmentId,
|
|
1723
|
+
claim_id: claimResult.claimId,
|
|
1724
|
+
}, dispatchCwd);
|
|
1608
1725
|
// pln#626 Phase 2 — the critique-only contract must reach the
|
|
1609
1726
|
// DELIVERED brief, not just the claim record: buildCoordinateBrief
|
|
1610
1727
|
// wraps this in a worker envelope, so prepend the constraint + the
|
|
@@ -1806,7 +1923,9 @@ export async function handleBclawLoop(args, ctx) {
|
|
|
1806
1923
|
}
|
|
1807
1924
|
// pln#562 step 4 — dispatching a turn hands work to another agent; gate
|
|
1808
1925
|
// it at the same trust bar as the other dispatch surfaces.
|
|
1809
|
-
|
|
1926
|
+
// pln#632 — `bind` also SPAWNS real workers (it dispatches the loop's linked
|
|
1927
|
+
// sequence), so it is gated at the same 'trusted' bar as turn-dispatch / coordinate.
|
|
1928
|
+
if ((args?.intent === 'turn' && args?.dispatch === true) || args?.intent === 'bind') {
|
|
1810
1929
|
const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'trusted', cwd, connectionSessionId);
|
|
1811
1930
|
if (resolved.error) {
|
|
1812
1931
|
return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
|
|
@@ -1814,7 +1933,7 @@ export async function handleBclawLoop(args, ctx) {
|
|
|
1814
1933
|
}
|
|
1815
1934
|
const { handleBclawLoop: runLoopIntent } = await import('./loops-handlers.js');
|
|
1816
1935
|
const targetCwd = resolveProjectCwd(args?.project, cwd);
|
|
1817
|
-
const result = runLoopIntent({ args: args, cwd: targetCwd });
|
|
1936
|
+
const result = await runLoopIntent({ args: args, cwd: targetCwd, sessionId: connectionSessionId });
|
|
1818
1937
|
return {
|
|
1819
1938
|
response: toolResponse({
|
|
1820
1939
|
content: [{ type: 'text', text: result.summary }],
|