brainclaw 1.27.0 → 1.28.1
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/register-coordination.js +12 -0
- package/dist/commands/loop.js +12 -0
- package/dist/commands/loops-handlers.js +261 -2
- package/dist/commands/mcp-catalog.js +16 -3
- package/dist/commands/mcp-schemas.generated.js +20 -0
- package/dist/commands/mcp-write-claims.js +55 -8
- package/dist/commands/mcp-write-coordination.js +3 -0
- package/dist/core/actions.js +17 -3
- package/dist/core/execution-adapters.js +29 -0
- package/dist/core/facade-schema.js +3 -0
- package/dist/core/loop-turn-dispatch.js +31 -3
- package/dist/core/loops/attempt-authority.js +20 -0
- package/dist/core/loops/brief-assembly.js +21 -4
- package/dist/core/loops/continuation.js +337 -0
- package/dist/core/loops/evidence.js +1 -0
- package/dist/core/loops/facade-schema.js +49 -1
- package/dist/core/loops/gate-policy.js +52 -4
- package/dist/core/loops/impl-bind.js +58 -6
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/reconcile-turn.js +2 -0
- package/dist/core/loops/result-reducers.js +15 -1
- package/dist/core/loops/store.js +4 -0
- package/dist/core/loops/types.js +20 -1
- package/dist/core/loops/verbs.js +3 -0
- package/dist/core/loops/verify-command.js +77 -15
- package/dist/core/reviewer-policy.js +39 -0
- package/dist/core/schema.js +21 -1
- package/dist/facts.js +7 -7
- package/dist/facts.json +6 -6
- package/docs/cli.md +4 -2
- package/docs/concepts/loop-engine.md +25 -0
- package/docs/loops/implementation.md +20 -0
- package/docs/mcp-schema-changelog.md +10 -1
- package/package.json +1 -1
|
Binary file
|
|
@@ -466,6 +466,18 @@ export function registerCoordinationCommands(program) {
|
|
|
466
466
|
const { runLoopCommand } = await import('../commands/loop.js');
|
|
467
467
|
await runLoopCommand('add-artifact', { loop_id }, options, globalOpts.cwd);
|
|
468
468
|
});
|
|
469
|
+
loopCmd
|
|
470
|
+
.command('continue <loop_id>')
|
|
471
|
+
.description('Evaluate and apply a persisted cross-loop continuation')
|
|
472
|
+
.option('--action-index <n>', 'Zero-based next_action index', '0')
|
|
473
|
+
.option('--autonomy-mode <mode>', 'autonomous, require_approval, or deny', 'autonomous')
|
|
474
|
+
.option('--risk <risk>', 'normal or protected', 'normal')
|
|
475
|
+
.option('--json', 'Machine-readable output')
|
|
476
|
+
.action(async (loop_id, options) => {
|
|
477
|
+
const globalOpts = program.opts();
|
|
478
|
+
const { runLoopCommand } = await import('../commands/loop.js');
|
|
479
|
+
await runLoopCommand('continue', { loop_id }, options, globalOpts.cwd);
|
|
480
|
+
});
|
|
469
481
|
// --- attempt-authority (two-release writer guard; P4) ---
|
|
470
482
|
const attemptAuthorityCmd = program
|
|
471
483
|
.command('attempt-authority')
|
package/dist/commands/loop.js
CHANGED
|
@@ -154,6 +154,18 @@ function buildRequest(subcommand, loopId, opts) {
|
|
|
154
154
|
ref: parseOptionalRef(opts.ref, opts),
|
|
155
155
|
},
|
|
156
156
|
};
|
|
157
|
+
case 'continue': {
|
|
158
|
+
const actionIndex = opts.actionIndex === undefined ? 0 : Number(opts.actionIndex);
|
|
159
|
+
if (!Number.isInteger(actionIndex) || actionIndex < 0)
|
|
160
|
+
fail('--action-index must be a non-negative integer', 1, opts);
|
|
161
|
+
return {
|
|
162
|
+
intent: 'continue',
|
|
163
|
+
loop_id: loopId,
|
|
164
|
+
action_index: actionIndex,
|
|
165
|
+
autonomy_mode: opts.autonomyMode ?? 'autonomous',
|
|
166
|
+
risk: opts.risk ?? 'normal',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
export async function runLoopCommand(subcommand, args, options = {}, cwd) {
|
|
@@ -5,7 +5,11 @@ import { dispatchLoopTurn } from '../core/loop-turn-dispatch.js';
|
|
|
5
5
|
import { findReservationByRunId } from '../core/loops/attempt-reservation.js';
|
|
6
6
|
import { runVerify } from '../core/loops/verify-command.js';
|
|
7
7
|
import { runImplBind } from '../core/loops/impl-bind.js';
|
|
8
|
-
import {
|
|
8
|
+
import { loadSequence } from '../core/sequence.js';
|
|
9
|
+
import { createActionRequired, loadActionRequired } from '../core/actions.js';
|
|
10
|
+
import { selectImplementationReviewer } from '../core/reviewer-policy.js';
|
|
11
|
+
import { handleBclawCoordinate } from './mcp-write-coordination.js';
|
|
12
|
+
import { add_artifact, advance, AwaitingFileApplyApprovalError, closeLoop, complete_turn, computeNextExpected, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, provideInput, requestInput, resume, sweepPauseTimeouts, takeoverLoopAttempt, readLocalAuthorityHome, turn, VersionConflictError, withLoopLock, artifactEvidenceDigest, attachContinuationActionRequired, ensureContinuation, } from '../core/loops/index.js';
|
|
9
13
|
import { BclawLoopRequestSchema, BCLAW_LOOP_INTENTS, } from '../core/loops/facade-schema.js';
|
|
10
14
|
// NextExpectedHint type now lives in src/core/loops/next-expected.ts
|
|
11
15
|
// (hoisted per can_e57c7782 follow-up so MCP facade + CLI share the
|
|
@@ -16,6 +20,10 @@ function resolveActor(req, defaultActor) {
|
|
|
16
20
|
return { actor, agentId };
|
|
17
21
|
}
|
|
18
22
|
function successResponse(intent, result, artifacts, side_effects, warnings, durationMs, summary) {
|
|
23
|
+
const resultLoop = result && typeof result === 'object' && 'loop' in result
|
|
24
|
+
? result.loop
|
|
25
|
+
: undefined;
|
|
26
|
+
const nextActions = resultLoop ? pipelineNextActions(resultLoop) : [];
|
|
19
27
|
return {
|
|
20
28
|
response: {
|
|
21
29
|
status: 'ok',
|
|
@@ -25,10 +33,114 @@ function successResponse(intent, result, artifacts, side_effects, warnings, dura
|
|
|
25
33
|
side_effects,
|
|
26
34
|
warnings,
|
|
27
35
|
duration_ms: durationMs,
|
|
36
|
+
...(nextActions.length > 0 ? { next_actions: nextActions } : {}),
|
|
28
37
|
},
|
|
29
38
|
summary,
|
|
30
39
|
};
|
|
31
40
|
}
|
|
41
|
+
/** Cross-loop affordances: explicit next calls, never hidden orchestration. */
|
|
42
|
+
function pipelineNextActions(loop) {
|
|
43
|
+
if (loop.kind === 'ideation') {
|
|
44
|
+
const draft = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'plan_draft');
|
|
45
|
+
if (!draft || (loop.current_phase !== 'synthesis' && loop.status !== 'completed'))
|
|
46
|
+
return [];
|
|
47
|
+
const planIds = loop.linked?.plan_ids ?? [];
|
|
48
|
+
const sequenceIds = loop.linked?.sequence_ids ?? [];
|
|
49
|
+
if (planIds.length > 0 && sequenceIds.length > 0) {
|
|
50
|
+
return [{
|
|
51
|
+
tool: 'bclaw_loop',
|
|
52
|
+
args: {
|
|
53
|
+
intent: 'continue', loop_id: loop.id, action_index: 0,
|
|
54
|
+
autonomy_mode: 'autonomous', risk: 'normal',
|
|
55
|
+
},
|
|
56
|
+
when: 'evaluate and apply the accepted synthesis through persisted continuation policy',
|
|
57
|
+
}];
|
|
58
|
+
}
|
|
59
|
+
return [{
|
|
60
|
+
tool: 'bclaw_create',
|
|
61
|
+
args: { entity: 'plan', text: draft.body ?? '<materialize the plan_draft artifact>', status: 'todo' },
|
|
62
|
+
when: 'materialize the synthesis before opening its implementation loop',
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
if (loop.kind === 'implementation' && loop.current_phase === 'execute' && loop.status === 'open') {
|
|
66
|
+
return loop.slots
|
|
67
|
+
.filter((slot) => slot.status === 'open')
|
|
68
|
+
.map((slot) => ({
|
|
69
|
+
tool: 'bclaw_loop',
|
|
70
|
+
args: {
|
|
71
|
+
intent: 'turn', loop_id: loop.id, slot_id: slot.slot_id,
|
|
72
|
+
input: loop.goal ?? loop.title, dispatch: true,
|
|
73
|
+
},
|
|
74
|
+
when: `dispatch implementation lane ${slot.lane ?? slot.role} through AttemptAuthority`,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
if (loop.kind === 'implementation' && (loop.current_phase === 'handoff_ready' || loop.status === 'completed')) {
|
|
78
|
+
return [{
|
|
79
|
+
tool: 'bclaw_loop',
|
|
80
|
+
args: {
|
|
81
|
+
intent: 'continue', loop_id: loop.id, action_index: 0,
|
|
82
|
+
autonomy_mode: 'autonomous', risk: 'normal',
|
|
83
|
+
},
|
|
84
|
+
when: 'evaluate and apply the attested handoff through persisted continuation policy',
|
|
85
|
+
}];
|
|
86
|
+
}
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
/** Concrete action evaluated by continuation policy; never exposed as an ungoverned hint. */
|
|
90
|
+
function proposedPipelineActions(loop, cwd) {
|
|
91
|
+
if (loop.kind === 'ideation') {
|
|
92
|
+
const draft = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'plan_draft');
|
|
93
|
+
const planIds = loop.linked?.plan_ids ?? [];
|
|
94
|
+
const sequenceIds = loop.linked?.sequence_ids ?? [];
|
|
95
|
+
if (!draft || planIds.length === 0 || sequenceIds.length !== 1)
|
|
96
|
+
return [];
|
|
97
|
+
const sequence = loadSequence(sequenceIds[0], cwd);
|
|
98
|
+
const lanes = [...new Set(sequence.items.map((item) => item.lane?.trim() || 'default'))].sort();
|
|
99
|
+
const sourceDigest = artifactEvidenceDigest(draft);
|
|
100
|
+
return [{
|
|
101
|
+
tool: 'bclaw_loop',
|
|
102
|
+
args: {
|
|
103
|
+
intent: 'open', kind: 'implementation', title: `Implement ${loop.title}`,
|
|
104
|
+
goal: loop.goal ?? loop.title,
|
|
105
|
+
linked: {
|
|
106
|
+
plan_ids: planIds, sequence_ids: sequenceIds, source_loop_id: loop.id,
|
|
107
|
+
source_artifact_id: draft.artifact_id, source_artifact_digest: sourceDigest,
|
|
108
|
+
},
|
|
109
|
+
verify: draft.implementation_verify,
|
|
110
|
+
slots: lanes.map((lane) => ({ role: 'implementer', lane })),
|
|
111
|
+
allow_orphan: true,
|
|
112
|
+
},
|
|
113
|
+
when: 'start implementation from the accepted synthesis',
|
|
114
|
+
}];
|
|
115
|
+
}
|
|
116
|
+
if (loop.kind === 'implementation' && (loop.current_phase === 'handoff_ready' || loop.status === 'completed')) {
|
|
117
|
+
const handoff = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'handoff');
|
|
118
|
+
if (!handoff?.ref)
|
|
119
|
+
return [];
|
|
120
|
+
const reviewer = selectImplementationReviewer(loop, cwd);
|
|
121
|
+
const reviewScope = [...new Set(loop.slots.map((slot) => slot.scope_hint?.trim()).filter((scope) => Boolean(scope)))].join(',');
|
|
122
|
+
const sourceDigest = artifactEvidenceDigest(handoff);
|
|
123
|
+
return [{
|
|
124
|
+
tool: 'bclaw_coordinate',
|
|
125
|
+
args: {
|
|
126
|
+
intent: 'review', open_loop: true, review_mode: 'asymmetric',
|
|
127
|
+
task: `Review implementation loop ${loop.id}; handoff ${handoff.ref.kind}:${handoff.ref.id}`,
|
|
128
|
+
targetAgents: [reviewer.agent],
|
|
129
|
+
...(reviewScope ? { scope: reviewScope } : {}),
|
|
130
|
+
...((handoff.ref.kind === 'commit' || handoff.ref.kind === 'branch') ? { ref: handoff.ref.id } : {}),
|
|
131
|
+
linked: {
|
|
132
|
+
source_loop_id: loop.id,
|
|
133
|
+
source_artifact_id: handoff.artifact_id,
|
|
134
|
+
source_artifact_digest: sourceDigest,
|
|
135
|
+
plan_ids: loop.linked?.plan_ids,
|
|
136
|
+
sequence_ids: loop.linked?.sequence_ids,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
when: `reviewer ${reviewer.agent} selected by ${reviewer.policy_version}`,
|
|
140
|
+
}];
|
|
141
|
+
}
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
32
144
|
function errorResponse(intent, code, message, durationMs, result = null) {
|
|
33
145
|
return {
|
|
34
146
|
response: {
|
|
@@ -182,6 +294,59 @@ function trySweepLoopTimeouts(loop_id, cwd) {
|
|
|
182
294
|
}
|
|
183
295
|
catch { /* best-effort: never block facade on sweep errors */ }
|
|
184
296
|
}
|
|
297
|
+
/** Execute a persisted continuation through the same public handler used by MCP/CLI callers. */
|
|
298
|
+
export async function executeContinuationPublicAction(record, options) {
|
|
299
|
+
const args = record.action.args ?? {};
|
|
300
|
+
const linked = (args.linked && typeof args.linked === 'object' ? args.linked : {});
|
|
301
|
+
const publicArgs = {
|
|
302
|
+
...args,
|
|
303
|
+
linked: { ...linked, continuation_key: record.continuation_key },
|
|
304
|
+
client_request_id: `ctn_${record.continuation_key}`,
|
|
305
|
+
agent: options.actor,
|
|
306
|
+
agentId: options.agentId,
|
|
307
|
+
};
|
|
308
|
+
let downstreamId;
|
|
309
|
+
if (record.action.tool === 'bclaw_loop') {
|
|
310
|
+
const opened = await handleBclawLoop({
|
|
311
|
+
args: publicArgs, cwd: options.cwd, defaultActor: options.actor, sessionId: options.sessionId,
|
|
312
|
+
});
|
|
313
|
+
if (opened.response.status !== 'ok')
|
|
314
|
+
throw new Error(opened.response.error ?? opened.summary);
|
|
315
|
+
downstreamId = opened.response.result.loop?.id;
|
|
316
|
+
}
|
|
317
|
+
else if (record.action.tool === 'bclaw_coordinate') {
|
|
318
|
+
const coordinateCwd = options.cwd ?? process.cwd();
|
|
319
|
+
const coordinated = await handleBclawCoordinate(publicArgs, {
|
|
320
|
+
cwd: coordinateCwd,
|
|
321
|
+
connectionSessionId: options.sessionId,
|
|
322
|
+
// The persisted source loop is an explicit store selector. Preserve that
|
|
323
|
+
// provenance so a multi-project workspace cannot reinterpret this as a
|
|
324
|
+
// bare-cwd review and reject or misroute the downstream loop.
|
|
325
|
+
effectiveScope: {
|
|
326
|
+
cwd: coordinateCwd,
|
|
327
|
+
active_source: 'explicit',
|
|
328
|
+
resolved_project: { path: coordinateCwd },
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
if (coordinated.response.isError) {
|
|
332
|
+
const details = coordinated.response.structuredContent;
|
|
333
|
+
throw new Error(details?.error ?? details?.message ?? 'continuation_coordinate_failed');
|
|
334
|
+
}
|
|
335
|
+
const facade = coordinated.response.structuredContent;
|
|
336
|
+
if (facade?.status === 'error')
|
|
337
|
+
throw new Error(facade.error ?? 'continuation_coordinate_failed');
|
|
338
|
+
downstreamId = facade?.result?.loop_id;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
throw new Error(`continuation_action_unsupported: ${record.action.tool}`);
|
|
342
|
+
}
|
|
343
|
+
if (!downstreamId)
|
|
344
|
+
throw new Error('continuation_open_missing_loop');
|
|
345
|
+
if (process.env.BRAINCLAW_TEST_FAULT_CONTINUATION_AFTER_OPEN === '1') {
|
|
346
|
+
throw new Error('fault_injection: continuation_after_open');
|
|
347
|
+
}
|
|
348
|
+
return { kind: 'loop', id: downstreamId };
|
|
349
|
+
}
|
|
185
350
|
export async function handleBclawLoop(options) {
|
|
186
351
|
const startMs = Date.now();
|
|
187
352
|
const defaultActor = options.defaultActor ?? 'bclaw_loop';
|
|
@@ -371,6 +536,7 @@ export async function handleBclawLoop(options) {
|
|
|
371
536
|
body: req.artifact.body,
|
|
372
537
|
ref: req.artifact.ref,
|
|
373
538
|
addresses_critique: req.artifact.addresses_critique,
|
|
539
|
+
implementation_verify: req.artifact.implementation_verify,
|
|
374
540
|
}
|
|
375
541
|
: undefined,
|
|
376
542
|
actor,
|
|
@@ -434,6 +600,7 @@ export async function handleBclawLoop(options) {
|
|
|
434
600
|
body: req.artifact.body,
|
|
435
601
|
ref: req.artifact.ref,
|
|
436
602
|
addresses_critique: req.artifact.addresses_critique,
|
|
603
|
+
implementation_verify: req.artifact.implementation_verify,
|
|
437
604
|
},
|
|
438
605
|
actor,
|
|
439
606
|
}, options.cwd);
|
|
@@ -522,7 +689,7 @@ export async function handleBclawLoop(options) {
|
|
|
522
689
|
return errorResponse('verify', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
523
690
|
}
|
|
524
691
|
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
525
|
-
const result = runVerify({ loop_id: req.loop_id, actor }, options.cwd);
|
|
692
|
+
const result = runVerify({ loop_id: req.loop_id, slot_id: req.slot_id, actor }, options.cwd);
|
|
526
693
|
const newEvents = findNewLoopEvents(result.thread.id, beforeEvents, options.cwd);
|
|
527
694
|
const summary = result.unconfigured
|
|
528
695
|
? `verify: loop has no protocol.verify — falling back to an agent-narrated verify_report`
|
|
@@ -537,6 +704,98 @@ export async function handleBclawLoop(options) {
|
|
|
537
704
|
next_expected: computeNextExpected(result.thread),
|
|
538
705
|
}, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summary);
|
|
539
706
|
}
|
|
707
|
+
case 'continue': {
|
|
708
|
+
const source = getLoop(req.loop_id, options.cwd);
|
|
709
|
+
if (!source) {
|
|
710
|
+
return errorResponse('continue', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
711
|
+
}
|
|
712
|
+
const actions = proposedPipelineActions(source, options.cwd);
|
|
713
|
+
const action = actions[req.action_index];
|
|
714
|
+
if (!action) {
|
|
715
|
+
return errorResponse('continue', 'continuation_unavailable', `no executable continuation action ${req.action_index} for ${source.id}`, Date.now() - startMs);
|
|
716
|
+
}
|
|
717
|
+
const sourceArtifactId = action.args?.linked?.source_artifact_id;
|
|
718
|
+
const sourceArtifact = source.artifacts.find((artifact) => artifact.artifact_id === sourceArtifactId);
|
|
719
|
+
if (!sourceArtifact) {
|
|
720
|
+
return errorResponse('continue', 'continuation_source_missing', 'source continuation artifact disappeared', Date.now() - startMs);
|
|
721
|
+
}
|
|
722
|
+
const ensured = await ensureContinuation({
|
|
723
|
+
source_loop: source,
|
|
724
|
+
source_artifact: sourceArtifact,
|
|
725
|
+
action,
|
|
726
|
+
action_index: req.action_index,
|
|
727
|
+
autonomy_mode: req.autonomy_mode,
|
|
728
|
+
risk: req.risk,
|
|
729
|
+
actor,
|
|
730
|
+
actor_id: agentId,
|
|
731
|
+
execute: (record) => executeContinuationPublicAction(record, {
|
|
732
|
+
cwd: options.cwd, actor, agentId, sessionId: options.sessionId,
|
|
733
|
+
}),
|
|
734
|
+
}, options.cwd);
|
|
735
|
+
let continuation = ensured.record;
|
|
736
|
+
if (continuation.state === 'approval_required') {
|
|
737
|
+
let approval = continuation.action_required_id
|
|
738
|
+
? loadActionRequired(continuation.action_required_id, options.cwd)
|
|
739
|
+
: undefined;
|
|
740
|
+
if (!approval) {
|
|
741
|
+
approval = createActionRequired({
|
|
742
|
+
target: { kind: 'continuation', continuation_id: continuation.id },
|
|
743
|
+
plan_id: source.linked?.plan_ids?.[0],
|
|
744
|
+
sequence_id: source.linked?.sequence_ids?.[0],
|
|
745
|
+
agent: actor,
|
|
746
|
+
agent_id: agentId,
|
|
747
|
+
session_id: options.sessionId,
|
|
748
|
+
kind: 'plan_approval',
|
|
749
|
+
scope: source.goal,
|
|
750
|
+
title: `Approve continuation from ${source.id}`,
|
|
751
|
+
prompt: continuation.reason.join('; '),
|
|
752
|
+
tags: ['loop-engine', 'continuation', 'approval-required'],
|
|
753
|
+
}, options.cwd);
|
|
754
|
+
continuation = attachContinuationActionRequired(continuation.id, approval.id, actor, agentId, options.cwd);
|
|
755
|
+
}
|
|
756
|
+
const handled = successResponse('continue', { continuation, action_required: approval }, [{ type: 'continuation', id: continuation.id }, { type: 'action', id: approval.id }], [{ action: 'create', entity: 'action', id: approval.id }], [], Date.now() - startMs, `continuation ${continuation.id} requires approval ${approval.id}`);
|
|
757
|
+
handled.response.next_actions = [{
|
|
758
|
+
tool: 'bclaw_assignment_action',
|
|
759
|
+
args: { action_id: approval.id, outcome: 'resolved' },
|
|
760
|
+
when: 'a different trusted supervisor approves this continuation',
|
|
761
|
+
}];
|
|
762
|
+
return handled;
|
|
763
|
+
}
|
|
764
|
+
if (continuation.state === 'denied') {
|
|
765
|
+
return successResponse('continue', { continuation }, [{ type: 'continuation', id: continuation.id }], [], [], Date.now() - startMs, `continuation ${continuation.id} denied: ${continuation.reason.join('; ')}`);
|
|
766
|
+
}
|
|
767
|
+
if (ensured.executing_elsewhere) {
|
|
768
|
+
const handled = successResponse('continue', { continuation, executing_elsewhere: true }, [{ type: 'continuation', id: continuation.id }], [], [], Date.now() - startMs, `continuation ${continuation.id} is applying in another live process`);
|
|
769
|
+
handled.response.next_actions = [{
|
|
770
|
+
tool: 'bclaw_loop',
|
|
771
|
+
args: { intent: 'continue', loop_id: source.id, action_index: req.action_index },
|
|
772
|
+
when: 'retry after the current continuation owner settles',
|
|
773
|
+
}];
|
|
774
|
+
return handled;
|
|
775
|
+
}
|
|
776
|
+
const downstreamId = continuation.downstream?.id;
|
|
777
|
+
if (!downstreamId)
|
|
778
|
+
throw new Error('continuation_applied_without_downstream');
|
|
779
|
+
let loop = getLoop(downstreamId, options.cwd);
|
|
780
|
+
if (!loop)
|
|
781
|
+
throw new Error('continuation_downstream_disappeared');
|
|
782
|
+
let bind;
|
|
783
|
+
if (loop.kind === 'implementation') {
|
|
784
|
+
const bound = await handleBclawLoop({
|
|
785
|
+
args: { intent: 'bind', loop_id: downstreamId, agent: actor, agentId },
|
|
786
|
+
cwd: options.cwd,
|
|
787
|
+
defaultActor: actor,
|
|
788
|
+
sessionId: options.sessionId,
|
|
789
|
+
});
|
|
790
|
+
if (bound.response.status !== 'ok')
|
|
791
|
+
throw new Error(bound.response.error ?? bound.summary);
|
|
792
|
+
bind = bound.response.result;
|
|
793
|
+
loop = getLoop(downstreamId, options.cwd);
|
|
794
|
+
if (!loop)
|
|
795
|
+
throw new Error('continuation_downstream_disappeared');
|
|
796
|
+
}
|
|
797
|
+
return successResponse('continue', { loop, continuation, ...(bind ? { bind } : {}), reused: ensured.reused, next_expected: computeNextExpected(loop) }, [{ type: 'continuation', id: continuation.id }, loopArtifactEntry(loop.id)], [{ action: 'update', entity: 'continuation', id: continuation.id }, sideEffectUpdate('loop', loop.id)], [], Date.now() - startMs, `continuation ${continuation.id} applied to ${loop.id} phase=${loop.current_phase}`);
|
|
798
|
+
}
|
|
540
799
|
case 'bind': {
|
|
541
800
|
// Implementation bind is engine-only: validate the linked sequence and
|
|
542
801
|
// advance bind -> execute. Worker launch belongs exclusively to
|
|
@@ -854,6 +854,16 @@ const MCP_WRITE_TOOLS = [
|
|
|
854
854
|
targetAgents: { type: 'array', items: { type: 'string' }, description: 'Agent names to target. If omitted, all spawnable agents are used.' },
|
|
855
855
|
constraints: { type: 'object', description: 'Optional structured constraints passed alongside the brief (e.g. deadline, reviewCriteria).' },
|
|
856
856
|
threadId: { type: 'string', description: 'Thread ID for summarize intent.' },
|
|
857
|
+
linked: {
|
|
858
|
+
type: 'object',
|
|
859
|
+
description: 'Optional pipeline provenance persisted on a review loop opened by this call.',
|
|
860
|
+
properties: {
|
|
861
|
+
plan_ids: { type: 'array', items: { type: 'string' } },
|
|
862
|
+
sequence_ids: { type: 'array', items: { type: 'string' } },
|
|
863
|
+
source_loop_id: { type: 'string', pattern: '^lop_[0-9a-z]+$' },
|
|
864
|
+
},
|
|
865
|
+
additionalProperties: false,
|
|
866
|
+
},
|
|
857
867
|
autoExecute: { type: 'boolean', description: 'Attempt to spawn target agents after delivery (default: true). Applies to the spawning intents assign/review/reroute AND to multi-agent ideate (with targetAgents, it spawns one worktree-isolated critic worker per target). 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 bash commands for the supervisor to run.' },
|
|
858
868
|
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.' },
|
|
859
869
|
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.' },
|
|
@@ -872,7 +882,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
872
882
|
},
|
|
873
883
|
{
|
|
874
884
|
name: 'bclaw_loop',
|
|
875
|
-
description: 'Loop engine facade: open/turn/complete_turn/takeover/advance/add_artifact/pause/resume/close/verify/request_input/provide_input/get/list multi-turn work loops (review, ideation, implementation, research, debug).
|
|
885
|
+
description: 'Loop engine facade: open/turn/complete_turn/takeover/advance/add_artifact/pause/resume/close/verify/bind/continue/request_input/provide_input/get/list multi-turn work loops (review, ideation, implementation, research, debug). `continue` evaluates and persists a cross-loop continuation, then invokes the same public open or coordinate-review path; implementation downstreams additionally bind. Direct open requires allow_orphan=true because the caller owns subsequent dispatch.',
|
|
876
886
|
// schemaSource is informational for now — grep target so future migrators
|
|
877
887
|
// can locate zod-derived tools quickly. The parity test in
|
|
878
888
|
// tests/unit/mcp-zod-parity.test.ts hard-codes its (tool, zod-schema)
|
|
@@ -890,8 +900,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
890
900
|
properties: {
|
|
891
901
|
intent: {
|
|
892
902
|
type: 'string',
|
|
893
|
-
enum: ['open', 'get', 'list', 'turn', 'complete_turn', 'takeover', 'advance', 'add_artifact', 'pause', 'resume', 'close', 'verify', 'bind', 'request_input', 'provide_input'],
|
|
894
|
-
description: 'Loop lifecycle intent. Review/ideation normally start via bclaw_coordinate; implementation/research/debug may use open with allow_orphan=true and then explicitly bind/turn/dispatch.
|
|
903
|
+
enum: ['open', 'get', 'list', 'turn', 'complete_turn', 'takeover', 'advance', 'add_artifact', 'pause', 'resume', 'close', 'verify', 'bind', 'continue', 'request_input', 'provide_input'],
|
|
904
|
+
description: 'Loop lifecycle intent. `continue` evaluates one next_action through persisted continuation policy and applies it through public open/bind semantics. Review/ideation normally start via bclaw_coordinate; implementation/research/debug may use open with allow_orphan=true and then explicitly bind/turn/dispatch.',
|
|
895
905
|
},
|
|
896
906
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
897
907
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -934,6 +944,9 @@ const MCP_WRITE_TOOLS = [
|
|
|
934
944
|
model: { type: 'string', description: 'turn dispatch: model override. Deprecated and ignored for engine-only bind.' },
|
|
935
945
|
target_agents: { type: 'array', items: { type: 'string' }, description: 'turn dispatch: deterministic capability candidate pool used when the slot has no frozen agent.' },
|
|
936
946
|
max_assignments: { type: 'number', description: 'Deprecated bind launch option retained for compatibility and ignored.' },
|
|
947
|
+
action_index: { type: 'number', description: 'continue: zero-based proposed next_action index (default 0).' },
|
|
948
|
+
autonomy_mode: { type: 'string', enum: ['autonomous', 'require_approval', 'deny'], description: 'continue: policy mode. require_approval creates ActionRequired; deny persists a terminal denial.' },
|
|
949
|
+
risk: { type: 'string', enum: ['normal', 'protected'], description: 'continue: protected risk always requires approval.' },
|
|
937
950
|
to_phase: { type: 'string', description: 'advance: explicit target phase (otherwise the next phase).' },
|
|
938
951
|
force: { type: 'boolean', description: 'advance: allow going backwards (increments iteration_count).' },
|
|
939
952
|
reason: { type: 'string', description: 'advance / pause / close: optional reason string.' },
|
|
@@ -283,6 +283,26 @@ export const generatedSchemas = {
|
|
|
283
283
|
"phase": {
|
|
284
284
|
"type": "string"
|
|
285
285
|
},
|
|
286
|
+
"lane": {
|
|
287
|
+
"type": "string"
|
|
288
|
+
},
|
|
289
|
+
"scope_hint": {
|
|
290
|
+
"type": "string"
|
|
291
|
+
},
|
|
292
|
+
"plan_ids": {
|
|
293
|
+
"type": "array",
|
|
294
|
+
"items": {
|
|
295
|
+
"type": "string",
|
|
296
|
+
"minLength": 1
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
"step_ids": {
|
|
300
|
+
"type": "array",
|
|
301
|
+
"items": {
|
|
302
|
+
"type": "string",
|
|
303
|
+
"minLength": 1
|
|
304
|
+
}
|
|
305
|
+
},
|
|
286
306
|
"status": {
|
|
287
307
|
"type": "string",
|
|
288
308
|
"enum": [
|
|
@@ -951,24 +951,71 @@ export async function handleBclawAssignmentAction(payload, ctx) {
|
|
|
951
951
|
if (pendingAction && pendingAction.agent === resolved.identity.agent_name) {
|
|
952
952
|
return { response: createToolErrorResponse('trust_error', `Agent '${resolved.identity.agent_name}' cannot resolve its own action. A supervisor or different agent must respond.`) };
|
|
953
953
|
}
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
954
|
+
const typedOutcome = outcome;
|
|
955
|
+
// Continuation approvals are safe to replay. This matters when the first
|
|
956
|
+
// response is lost after the downstream loop was created: the supervisor
|
|
957
|
+
// can submit the same decision again and observe the same continuation.
|
|
958
|
+
const continuationReplay = pendingAction?.target?.kind === 'continuation'
|
|
959
|
+
&& pendingAction.status === typedOutcome;
|
|
960
|
+
const action = continuationReplay
|
|
961
|
+
? pendingAction
|
|
962
|
+
: resolveActionRequired(actionId, {
|
|
963
|
+
outcome: typedOutcome,
|
|
964
|
+
text: typeof args.text === 'string' ? args.text : undefined,
|
|
965
|
+
payload: args.payload && typeof args.payload === 'object' ? args.payload : undefined,
|
|
966
|
+
responded_by: resolved.identity.agent_name,
|
|
967
|
+
responded_by_id: resolved.identity.agent_id,
|
|
968
|
+
session_id: connectionSessionId ?? 'unknown',
|
|
969
|
+
}, cwd);
|
|
970
|
+
let continuationResult;
|
|
971
|
+
if (action.target?.kind === 'continuation') {
|
|
972
|
+
const { denyContinuation, resumeApprovedContinuation } = await import('../core/loops/continuation.js');
|
|
973
|
+
if (outcome === 'resolved') {
|
|
974
|
+
const { executeContinuationPublicAction, handleBclawLoop } = await import('./loops-handlers.js');
|
|
975
|
+
const resumed = await resumeApprovedContinuation(action.target.continuation_id, action.id, resolved.identity.agent_name, resolved.identity.agent_id, (record) => executeContinuationPublicAction(record, {
|
|
976
|
+
cwd,
|
|
977
|
+
actor: resolved.identity.agent_name,
|
|
978
|
+
agentId: resolved.identity.agent_id,
|
|
979
|
+
sessionId: connectionSessionId,
|
|
980
|
+
}), cwd);
|
|
981
|
+
const downstreamId = resumed.record.downstream?.id;
|
|
982
|
+
let bind;
|
|
983
|
+
if (downstreamId) {
|
|
984
|
+
const { getLoop } = await import('../core/loops/store.js');
|
|
985
|
+
if (getLoop(downstreamId, cwd)?.kind === 'implementation') {
|
|
986
|
+
const handled = await handleBclawLoop({
|
|
987
|
+
args: {
|
|
988
|
+
intent: 'bind', loop_id: downstreamId,
|
|
989
|
+
agent: resolved.identity.agent_name, agentId: resolved.identity.agent_id,
|
|
990
|
+
},
|
|
991
|
+
cwd,
|
|
992
|
+
defaultActor: resolved.identity.agent_name,
|
|
993
|
+
sessionId: connectionSessionId,
|
|
994
|
+
});
|
|
995
|
+
if (handled.response.status !== 'ok')
|
|
996
|
+
throw new Error(handled.response.error ?? handled.summary);
|
|
997
|
+
bind = handled.response.result;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
continuationResult = { continuation: resumed.record, bind };
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
const denied = denyContinuation(action.target.continuation_id, `approval ${action.id} ${outcome}`, resolved.identity.agent_name, resolved.identity.agent_id, cwd);
|
|
1004
|
+
continuationResult = { continuation: denied };
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
962
1007
|
return {
|
|
963
1008
|
response: {
|
|
964
1009
|
content: [{ type: 'text', text: `Action ${actionId} ${action.status}` }],
|
|
965
1010
|
structuredContent: {
|
|
966
1011
|
action_id: action.id,
|
|
967
1012
|
assignment_id: action.assignment_id,
|
|
1013
|
+
target: action.target,
|
|
968
1014
|
run_id: action.run_id,
|
|
969
1015
|
status: action.status,
|
|
970
1016
|
resolved_at: action.resolved_at,
|
|
971
1017
|
response: action.response,
|
|
1018
|
+
...(continuationResult ? { continuation_result: continuationResult } : {}),
|
|
972
1019
|
},
|
|
973
1020
|
},
|
|
974
1021
|
};
|
|
@@ -1002,6 +1002,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1002
1002
|
created_by: creatorActor,
|
|
1003
1003
|
slots,
|
|
1004
1004
|
mode: req.review_mode ?? 'asymmetric',
|
|
1005
|
+
linked: req.linked,
|
|
1005
1006
|
}, dispatchCwd);
|
|
1006
1007
|
out.loopId = loop.id;
|
|
1007
1008
|
out.artifacts.push({ type: 'loop', id: loop.id });
|
|
@@ -1585,6 +1586,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1585
1586
|
goal: req.scope,
|
|
1586
1587
|
created_by: creatorActor,
|
|
1587
1588
|
slots,
|
|
1589
|
+
linked: req.linked,
|
|
1588
1590
|
...(presetSelected
|
|
1589
1591
|
? {
|
|
1590
1592
|
phases: presetSelected.phases,
|
|
@@ -1677,6 +1679,7 @@ export async function handleBclawCoordinate(args, ctx) {
|
|
|
1677
1679
|
category,
|
|
1678
1680
|
text: r.text,
|
|
1679
1681
|
score: r.score,
|
|
1682
|
+
relatedPaths: r.related_paths,
|
|
1680
1683
|
}));
|
|
1681
1684
|
},
|
|
1682
1685
|
};
|
package/dist/core/actions.js
CHANGED
|
@@ -11,6 +11,7 @@ import { emitRegistryPostImage, registryFaultPoint } from './events/registry-pos
|
|
|
11
11
|
import { createRuntimeEvent } from './events.js';
|
|
12
12
|
import { loadAssignment, transitionAssignment } from './assignments.js';
|
|
13
13
|
import { loadAgentRun, transitionAgentRun } from './agentruns.js';
|
|
14
|
+
import { denyContinuation } from './loops/continuation.js';
|
|
14
15
|
function actionsDir(cwd, mode = 'read') {
|
|
15
16
|
return resolveEntityDir('actions', cwd ?? process.cwd(), mode);
|
|
16
17
|
}
|
|
@@ -90,7 +91,7 @@ function expireStaleActions(actions, cwd) {
|
|
|
90
91
|
}
|
|
91
92
|
catch { /* best-effort */ }
|
|
92
93
|
try {
|
|
93
|
-
const assignment = loadAssignment(action.assignment_id, cwd);
|
|
94
|
+
const assignment = action.assignment_id ? loadAssignment(action.assignment_id, cwd) : undefined;
|
|
94
95
|
if (assignment && assignment.status === 'blocked') {
|
|
95
96
|
transitionAssignment(assignment.id, 'failed', {
|
|
96
97
|
actor: action.agent,
|
|
@@ -102,6 +103,12 @@ function expireStaleActions(actions, cwd) {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
catch { /* best-effort */ }
|
|
106
|
+
try {
|
|
107
|
+
if (action.target?.kind === 'continuation') {
|
|
108
|
+
denyContinuation(action.target.continuation_id, `approval ${action.id} expired`, action.agent, action.agent_id, cwd);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { /* best-effort */ }
|
|
105
112
|
try {
|
|
106
113
|
appendAuditEntry({
|
|
107
114
|
actor: action.agent,
|
|
@@ -184,11 +191,15 @@ function saveActionRequired(action, cwd) {
|
|
|
184
191
|
export function createActionRequired(options, cwd) {
|
|
185
192
|
const generated = generateIdWithLabel('actions', cwd);
|
|
186
193
|
const now = nowISO();
|
|
194
|
+
const target = options.target ?? (options.assignment_id
|
|
195
|
+
? { kind: 'assignment', assignment_id: options.assignment_id }
|
|
196
|
+
: undefined);
|
|
187
197
|
const action = ActionRequiredSchema.parse({
|
|
188
198
|
schema_version: 1,
|
|
189
199
|
id: generated.id,
|
|
190
200
|
short_label: generated.short_label,
|
|
191
201
|
assignment_id: options.assignment_id,
|
|
202
|
+
target,
|
|
192
203
|
run_id: options.run_id,
|
|
193
204
|
claim_id: options.claim_id,
|
|
194
205
|
message_id: options.message_id,
|
|
@@ -216,7 +227,7 @@ export function createActionRequired(options, cwd) {
|
|
|
216
227
|
action: 'create',
|
|
217
228
|
item_id: action.id,
|
|
218
229
|
item_type: 'state',
|
|
219
|
-
after: { kind: action.kind, assignment_id: action.assignment_id, run_id: action.run_id },
|
|
230
|
+
after: { kind: action.kind, target: action.target, assignment_id: action.assignment_id, run_id: action.run_id },
|
|
220
231
|
scope: action.scope,
|
|
221
232
|
session_id: action.session_id,
|
|
222
233
|
}, cwd);
|
|
@@ -268,6 +279,9 @@ export function resolveActionRequired(id, options, cwd) {
|
|
|
268
279
|
responded_at: now,
|
|
269
280
|
};
|
|
270
281
|
saveActionRequired(action, cwd);
|
|
282
|
+
if (action.target?.kind === 'continuation' && options.outcome !== 'resolved') {
|
|
283
|
+
denyContinuation(action.target.continuation_id, `approval ${action.id} ${options.outcome}`, options.responded_by, options.responded_by_id, cwd);
|
|
284
|
+
}
|
|
271
285
|
appendAuditEntry({
|
|
272
286
|
actor: options.responded_by,
|
|
273
287
|
actor_id: options.responded_by_id,
|
|
@@ -301,7 +315,7 @@ export function resolveActionRequired(id, options, cwd) {
|
|
|
301
315
|
}
|
|
302
316
|
}
|
|
303
317
|
}
|
|
304
|
-
const assignment = loadAssignment(action.assignment_id, cwd);
|
|
318
|
+
const assignment = action.assignment_id ? loadAssignment(action.assignment_id, cwd) : undefined;
|
|
305
319
|
if (assignment) {
|
|
306
320
|
if (options.outcome === 'resolved' && assignment.status === 'blocked') {
|
|
307
321
|
transitionAssignment(assignment.id, 'started', {
|