brainclaw 1.28.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 +220 -16
- package/dist/commands/mcp-catalog.js +6 -3
- package/dist/commands/mcp-write-claims.js +55 -8
- package/dist/core/actions.js +17 -3
- package/dist/core/loops/continuation.js +337 -0
- package/dist/core/loops/facade-schema.js +15 -0
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/types.js +6 -0
- package/dist/core/reviewer-policy.js +39 -0
- package/dist/core/schema.js +16 -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/mcp-schema-changelog.md +6 -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
|
|
@@ -46,12 +50,10 @@ function pipelineNextActions(loop) {
|
|
|
46
50
|
return [{
|
|
47
51
|
tool: 'bclaw_loop',
|
|
48
52
|
args: {
|
|
49
|
-
intent: '
|
|
50
|
-
|
|
51
|
-
verify: draft.implementation_verify,
|
|
52
|
-
slots: [{ role: 'implementer' }], allow_orphan: true,
|
|
53
|
+
intent: 'continue', loop_id: loop.id, action_index: 0,
|
|
54
|
+
autonomy_mode: 'autonomous', risk: 'normal',
|
|
53
55
|
},
|
|
54
|
-
when: '
|
|
56
|
+
when: 'evaluate and apply the accepted synthesis through persisted continuation policy',
|
|
55
57
|
}];
|
|
56
58
|
}
|
|
57
59
|
return [{
|
|
@@ -60,24 +62,81 @@ function pipelineNextActions(loop) {
|
|
|
60
62
|
when: 'materialize the synthesis before opening its implementation loop',
|
|
61
63
|
}];
|
|
62
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
|
+
}
|
|
63
116
|
if (loop.kind === 'implementation' && (loop.current_phase === 'handoff_ready' || loop.status === 'completed')) {
|
|
64
117
|
const handoff = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'handoff');
|
|
118
|
+
if (!handoff?.ref)
|
|
119
|
+
return [];
|
|
120
|
+
const reviewer = selectImplementationReviewer(loop, cwd);
|
|
65
121
|
const reviewScope = [...new Set(loop.slots.map((slot) => slot.scope_hint?.trim()).filter((scope) => Boolean(scope)))].join(',');
|
|
122
|
+
const sourceDigest = artifactEvidenceDigest(handoff);
|
|
66
123
|
return [{
|
|
67
124
|
tool: 'bclaw_coordinate',
|
|
68
125
|
args: {
|
|
69
|
-
intent: 'review', open_loop: true,
|
|
70
|
-
task: handoff
|
|
71
|
-
|
|
72
|
-
: `Review implementation loop ${loop.id} (${loop.title})`,
|
|
73
|
-
targetAgents: ['<reviewer>'],
|
|
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],
|
|
74
129
|
...(reviewScope ? { scope: reviewScope } : {}),
|
|
75
|
-
...(
|
|
76
|
-
|
|
77
|
-
:
|
|
78
|
-
|
|
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
|
+
},
|
|
79
138
|
},
|
|
80
|
-
when:
|
|
139
|
+
when: `reviewer ${reviewer.agent} selected by ${reviewer.policy_version}`,
|
|
81
140
|
}];
|
|
82
141
|
}
|
|
83
142
|
return [];
|
|
@@ -235,6 +294,59 @@ function trySweepLoopTimeouts(loop_id, cwd) {
|
|
|
235
294
|
}
|
|
236
295
|
catch { /* best-effort: never block facade on sweep errors */ }
|
|
237
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
|
+
}
|
|
238
350
|
export async function handleBclawLoop(options) {
|
|
239
351
|
const startMs = Date.now();
|
|
240
352
|
const defaultActor = options.defaultActor ?? 'bclaw_loop';
|
|
@@ -592,6 +704,98 @@ export async function handleBclawLoop(options) {
|
|
|
592
704
|
next_expected: computeNextExpected(result.thread),
|
|
593
705
|
}, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summary);
|
|
594
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
|
+
}
|
|
595
799
|
case 'bind': {
|
|
596
800
|
// Implementation bind is engine-only: validate the linked sequence and
|
|
597
801
|
// advance bind -> execute. Worker launch belongs exclusively to
|
|
@@ -882,7 +882,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
882
882
|
},
|
|
883
883
|
{
|
|
884
884
|
name: 'bclaw_loop',
|
|
885
|
-
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.',
|
|
886
886
|
// schemaSource is informational for now — grep target so future migrators
|
|
887
887
|
// can locate zod-derived tools quickly. The parity test in
|
|
888
888
|
// tests/unit/mcp-zod-parity.test.ts hard-codes its (tool, zod-schema)
|
|
@@ -900,8 +900,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
900
900
|
properties: {
|
|
901
901
|
intent: {
|
|
902
902
|
type: 'string',
|
|
903
|
-
enum: ['open', 'get', 'list', 'turn', 'complete_turn', 'takeover', 'advance', 'add_artifact', 'pause', 'resume', 'close', 'verify', 'bind', 'request_input', 'provide_input'],
|
|
904
|
-
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.',
|
|
905
905
|
},
|
|
906
906
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
907
907
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -944,6 +944,9 @@ const MCP_WRITE_TOOLS = [
|
|
|
944
944
|
model: { type: 'string', description: 'turn dispatch: model override. Deprecated and ignored for engine-only bind.' },
|
|
945
945
|
target_agents: { type: 'array', items: { type: 'string' }, description: 'turn dispatch: deterministic capability candidate pool used when the slot has no frozen agent.' },
|
|
946
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.' },
|
|
947
950
|
to_phase: { type: 'string', description: 'advance: explicit target phase (otherwise the next phase).' },
|
|
948
951
|
force: { type: 'boolean', description: 'advance: allow going backwards (increments iteration_count).' },
|
|
949
952
|
reason: { type: 'string', description: 'advance / pause / close: optional reason string.' },
|
|
@@ -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
|
};
|
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', {
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { NextActionSchema } from '../facade-schema.js';
|
|
7
|
+
import { memoryDir, writeFileAtomic } from '../io.js';
|
|
8
|
+
import { nowISO } from '../ids.js';
|
|
9
|
+
import { mutate } from '../mutation-pipeline.js';
|
|
10
|
+
import { appendAuditEntry } from '../audit.js';
|
|
11
|
+
import { createRuntimeEvent } from '../events.js';
|
|
12
|
+
import { artifactEvidenceDigest, validateArtifactEvidence } from './evidence.js';
|
|
13
|
+
import { getLoop, listLoops } from './store.js';
|
|
14
|
+
export const CONTINUATION_POLICY_VERSION = 'continuation-policy-v1';
|
|
15
|
+
export const ContinuationDecisionSchema = z.enum(['auto', 'require_approval', 'deny']);
|
|
16
|
+
export const ContinuationStateSchema = z.enum([
|
|
17
|
+
'proposed',
|
|
18
|
+
'approval_required',
|
|
19
|
+
'denied',
|
|
20
|
+
'applying',
|
|
21
|
+
'applied',
|
|
22
|
+
'failed_recoverable',
|
|
23
|
+
]);
|
|
24
|
+
const ContinuationOwnerSchema = z.object({
|
|
25
|
+
token: z.string().min(1),
|
|
26
|
+
pid: z.number().int().positive(),
|
|
27
|
+
host_id: z.string().min(1),
|
|
28
|
+
started_at: z.string(),
|
|
29
|
+
});
|
|
30
|
+
export const ContinuationRecordSchema = z.object({
|
|
31
|
+
schema_version: z.literal(1),
|
|
32
|
+
id: z.string().regex(/^ctn_[a-f0-9]{24}$/),
|
|
33
|
+
continuation_key: z.string().regex(/^[a-f0-9]{64}$/),
|
|
34
|
+
policy_version: z.literal(CONTINUATION_POLICY_VERSION),
|
|
35
|
+
source_loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
36
|
+
source_iteration: z.number().int().nonnegative(),
|
|
37
|
+
source_artifact_id: z.string().regex(/^art_[0-9a-z]+$/),
|
|
38
|
+
source_artifact_digest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
39
|
+
action_index: z.number().int().nonnegative(),
|
|
40
|
+
action_hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
41
|
+
action: NextActionSchema,
|
|
42
|
+
autonomy_mode: z.enum(['autonomous', 'require_approval', 'deny']),
|
|
43
|
+
risk: z.enum(['normal', 'protected']),
|
|
44
|
+
decision: ContinuationDecisionSchema,
|
|
45
|
+
reason: z.array(z.string().min(1)).min(1),
|
|
46
|
+
state: ContinuationStateSchema,
|
|
47
|
+
downstream: z.object({ kind: z.literal('loop'), id: z.string().regex(/^lop_[0-9a-z]+$/) }).optional(),
|
|
48
|
+
action_required_id: z.string().regex(/^act_[0-9a-z]+$/).optional(),
|
|
49
|
+
owner: ContinuationOwnerSchema.optional(),
|
|
50
|
+
last_error: z.string().optional(),
|
|
51
|
+
created_at: z.string(),
|
|
52
|
+
updated_at: z.string(),
|
|
53
|
+
});
|
|
54
|
+
function continuationsDir(cwd) {
|
|
55
|
+
return path.join(memoryDir(cwd ?? process.cwd()), 'loops', 'continuations');
|
|
56
|
+
}
|
|
57
|
+
function continuationPath(key, cwd) {
|
|
58
|
+
return path.join(continuationsDir(cwd), `${key}.json`);
|
|
59
|
+
}
|
|
60
|
+
function canonicalize(value) {
|
|
61
|
+
if (Array.isArray(value))
|
|
62
|
+
return value.map(canonicalize);
|
|
63
|
+
if (value && typeof value === 'object') {
|
|
64
|
+
return Object.fromEntries(Object.entries(value)
|
|
65
|
+
.filter(([, child]) => child !== undefined)
|
|
66
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
67
|
+
.map(([key, child]) => [key, canonicalize(child)]));
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function digest(value) {
|
|
72
|
+
return crypto.createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex');
|
|
73
|
+
}
|
|
74
|
+
function writeRecord(record, cwd) {
|
|
75
|
+
const parsed = ContinuationRecordSchema.parse(record);
|
|
76
|
+
fs.mkdirSync(continuationsDir(cwd), { recursive: true });
|
|
77
|
+
writeFileAtomic(continuationPath(parsed.continuation_key, cwd), `${JSON.stringify(parsed, null, 2)}\n`);
|
|
78
|
+
}
|
|
79
|
+
export function loadContinuation(idOrKey, cwd) {
|
|
80
|
+
const dir = continuationsDir(cwd);
|
|
81
|
+
if (!fs.existsSync(dir))
|
|
82
|
+
return undefined;
|
|
83
|
+
if (/^[a-f0-9]{64}$/.test(idOrKey)) {
|
|
84
|
+
const file = continuationPath(idOrKey, cwd);
|
|
85
|
+
if (!fs.existsSync(file))
|
|
86
|
+
return undefined;
|
|
87
|
+
return ContinuationRecordSchema.parse(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
88
|
+
}
|
|
89
|
+
for (const name of fs.readdirSync(dir).filter((entry) => entry.endsWith('.json'))) {
|
|
90
|
+
const record = ContinuationRecordSchema.parse(JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')));
|
|
91
|
+
if (record.id === idOrKey)
|
|
92
|
+
return record;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
export function listContinuations(cwd) {
|
|
97
|
+
const dir = continuationsDir(cwd);
|
|
98
|
+
if (!fs.existsSync(dir))
|
|
99
|
+
return [];
|
|
100
|
+
return fs.readdirSync(dir).filter((entry) => entry.endsWith('.json')).map((entry) => ContinuationRecordSchema.parse(JSON.parse(fs.readFileSync(path.join(dir, entry), 'utf8')))).sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
101
|
+
}
|
|
102
|
+
function findDownstream(key, cwd) {
|
|
103
|
+
const matches = listLoops({}, cwd).filter((loop) => loop.linked?.continuation_key === key);
|
|
104
|
+
if (matches.length > 1) {
|
|
105
|
+
throw new Error(`continuation_ambiguity: ${key} is linked to ${matches.length} downstream loops`);
|
|
106
|
+
}
|
|
107
|
+
return matches[0];
|
|
108
|
+
}
|
|
109
|
+
function containsPlaceholder(value) {
|
|
110
|
+
if (typeof value === 'string')
|
|
111
|
+
return /<[^>]+>/.test(value);
|
|
112
|
+
if (Array.isArray(value))
|
|
113
|
+
return value.some(containsPlaceholder);
|
|
114
|
+
return Boolean(value && typeof value === 'object' && Object.values(value).some(containsPlaceholder));
|
|
115
|
+
}
|
|
116
|
+
export function evaluateContinuation(input) {
|
|
117
|
+
const evidence = validateArtifactEvidence(input.source_loop, input.source_artifact);
|
|
118
|
+
if (!evidence.valid)
|
|
119
|
+
throw new Error(`continuation_source_unattested: ${evidence.reasons.join(',')}`);
|
|
120
|
+
if (containsPlaceholder(input.action))
|
|
121
|
+
throw new Error('continuation_action_placeholder: action is not executable');
|
|
122
|
+
const args = input.action.args ?? {};
|
|
123
|
+
const ideationToImplementation = input.source_loop.kind === 'ideation'
|
|
124
|
+
&& input.source_artifact.type === 'plan_draft'
|
|
125
|
+
&& Boolean(input.source_artifact.implementation_verify)
|
|
126
|
+
&& input.action.tool === 'bclaw_loop'
|
|
127
|
+
&& args.intent === 'open'
|
|
128
|
+
&& args.kind === 'implementation';
|
|
129
|
+
const targets = Array.isArray(args.targetAgents) ? args.targetAgents : [];
|
|
130
|
+
const implementationToReview = input.source_loop.kind === 'implementation'
|
|
131
|
+
&& input.source_artifact.type === 'handoff'
|
|
132
|
+
&& Boolean(input.source_artifact.ref)
|
|
133
|
+
&& input.action.tool === 'bclaw_coordinate'
|
|
134
|
+
&& args.intent === 'review'
|
|
135
|
+
&& args.open_loop === true
|
|
136
|
+
&& targets.length === 1;
|
|
137
|
+
if (!ideationToImplementation && !implementationToReview) {
|
|
138
|
+
throw new Error('continuation_action_unsupported: expected Ideation→Implementation or Implementation→Review');
|
|
139
|
+
}
|
|
140
|
+
const sourceDigest = artifactEvidenceDigest(input.source_artifact);
|
|
141
|
+
const actionHash = digest(input.action);
|
|
142
|
+
const continuationKey = digest({
|
|
143
|
+
source_loop_id: input.source_loop.id,
|
|
144
|
+
source_iteration: input.source_artifact.iteration ?? input.source_loop.iteration_count,
|
|
145
|
+
source_artifact_digest: sourceDigest,
|
|
146
|
+
canonical_action_hash: actionHash,
|
|
147
|
+
policy_version: CONTINUATION_POLICY_VERSION,
|
|
148
|
+
});
|
|
149
|
+
const decision = input.autonomy_mode === 'deny'
|
|
150
|
+
? 'deny'
|
|
151
|
+
: input.autonomy_mode === 'require_approval' || input.risk === 'protected'
|
|
152
|
+
? 'require_approval'
|
|
153
|
+
: 'auto';
|
|
154
|
+
const evidenceReason = ideationToImplementation ? 'attested ideation plan_draft' : 'attested implementation handoff';
|
|
155
|
+
const actionReason = ideationToImplementation ? 'concrete implementation action' : 'concrete independent review action';
|
|
156
|
+
const reason = decision === 'auto'
|
|
157
|
+
? [evidenceReason, actionReason, 'normal risk under autonomous mode']
|
|
158
|
+
: decision === 'require_approval'
|
|
159
|
+
? [input.risk === 'protected' ? 'protected risk requires operator approval' : 'project autonomy mode requires approval']
|
|
160
|
+
: ['project autonomy mode denies continuation'];
|
|
161
|
+
return { continuation_key: continuationKey, source_artifact_digest: sourceDigest, action_hash: actionHash, decision, reason };
|
|
162
|
+
}
|
|
163
|
+
function ownerAlive(owner) {
|
|
164
|
+
if (owner.host_id !== os.hostname())
|
|
165
|
+
return true;
|
|
166
|
+
try {
|
|
167
|
+
process.kill(owner.pid, 0);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return error.code === 'EPERM';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function audit(record, before, actor, actorId, cwd) {
|
|
175
|
+
appendAuditEntry({
|
|
176
|
+
actor, actor_id: actorId, action: before ? 'update' : 'create', item_id: record.id,
|
|
177
|
+
item_type: 'state', before: before ? { state: before } : undefined,
|
|
178
|
+
after: { state: record.state, decision: record.decision, continuation_key: record.continuation_key, downstream: record.downstream },
|
|
179
|
+
}, cwd);
|
|
180
|
+
createRuntimeEvent({
|
|
181
|
+
agent: actor, agent_id: actorId, event_type: 'observation',
|
|
182
|
+
text: `Continuation ${record.decision}: ${record.source_loop_id} → ${record.downstream?.id ?? record.state}`,
|
|
183
|
+
tags: ['loop-engine', 'continuation', `decision:${record.decision}`, `state:${record.state}`],
|
|
184
|
+
metadata: { protocol: CONTINUATION_POLICY_VERSION, continuation_id: record.id, continuation_key: record.continuation_key },
|
|
185
|
+
}, cwd);
|
|
186
|
+
}
|
|
187
|
+
export async function ensureContinuation(input, cwd) {
|
|
188
|
+
const proposal = evaluateContinuation(input);
|
|
189
|
+
const prepared = mutate({ cwd }, () => {
|
|
190
|
+
const existing = loadContinuation(proposal.continuation_key, cwd);
|
|
191
|
+
if (existing && existing.action_hash !== proposal.action_hash) {
|
|
192
|
+
throw new Error(`continuation_key_conflict: stored=${existing.action_hash} submitted=${proposal.action_hash}`);
|
|
193
|
+
}
|
|
194
|
+
const downstream = findDownstream(proposal.continuation_key, cwd);
|
|
195
|
+
if (downstream) {
|
|
196
|
+
const next = existing
|
|
197
|
+
? { ...existing, state: 'applied', downstream: { kind: 'loop', id: downstream.id }, owner: undefined, updated_at: nowISO() }
|
|
198
|
+
: undefined;
|
|
199
|
+
if (!next)
|
|
200
|
+
throw new Error('continuation_projection_missing: downstream exists without a continuation record');
|
|
201
|
+
writeRecord(next, cwd);
|
|
202
|
+
return { record: next, shouldExecute: false, reused: true };
|
|
203
|
+
}
|
|
204
|
+
if (existing?.state === 'applied' && existing.downstream)
|
|
205
|
+
return { record: existing, shouldExecute: false, reused: true };
|
|
206
|
+
if (existing?.state === 'denied' || existing?.state === 'approval_required') {
|
|
207
|
+
return { record: existing, shouldExecute: false, reused: true };
|
|
208
|
+
}
|
|
209
|
+
if (existing?.state === 'applying' && existing.owner && ownerAlive(existing.owner)) {
|
|
210
|
+
return { record: existing, shouldExecute: false, reused: true, executingElsewhere: true };
|
|
211
|
+
}
|
|
212
|
+
const now = nowISO();
|
|
213
|
+
const owner = { token: crypto.randomUUID(), pid: process.pid, host_id: os.hostname(), started_at: now };
|
|
214
|
+
const base = existing ?? {
|
|
215
|
+
schema_version: 1,
|
|
216
|
+
id: `ctn_${proposal.continuation_key.slice(0, 24)}`,
|
|
217
|
+
continuation_key: proposal.continuation_key,
|
|
218
|
+
policy_version: CONTINUATION_POLICY_VERSION,
|
|
219
|
+
source_loop_id: input.source_loop.id,
|
|
220
|
+
source_iteration: input.source_artifact.iteration ?? input.source_loop.iteration_count,
|
|
221
|
+
source_artifact_id: input.source_artifact.artifact_id,
|
|
222
|
+
source_artifact_digest: proposal.source_artifact_digest,
|
|
223
|
+
action_index: input.action_index,
|
|
224
|
+
action_hash: proposal.action_hash,
|
|
225
|
+
action: input.action,
|
|
226
|
+
autonomy_mode: input.autonomy_mode,
|
|
227
|
+
risk: input.risk,
|
|
228
|
+
decision: proposal.decision,
|
|
229
|
+
reason: proposal.reason,
|
|
230
|
+
state: 'proposed',
|
|
231
|
+
created_at: now,
|
|
232
|
+
updated_at: now,
|
|
233
|
+
};
|
|
234
|
+
const state = proposal.decision === 'deny' ? 'denied' : proposal.decision === 'require_approval' ? 'approval_required' : 'applying';
|
|
235
|
+
const record = {
|
|
236
|
+
...base,
|
|
237
|
+
decision: proposal.decision,
|
|
238
|
+
reason: existing?.reason ?? proposal.reason,
|
|
239
|
+
state,
|
|
240
|
+
owner: state === 'applying' ? owner : undefined,
|
|
241
|
+
updated_at: now,
|
|
242
|
+
};
|
|
243
|
+
writeRecord(record, cwd);
|
|
244
|
+
audit(record, existing?.state, input.actor, input.actor_id, cwd);
|
|
245
|
+
return { record, shouldExecute: state === 'applying', reused: Boolean(existing) };
|
|
246
|
+
});
|
|
247
|
+
if (!prepared.shouldExecute) {
|
|
248
|
+
return { record: prepared.record, reused: prepared.reused, executing_elsewhere: prepared.executingElsewhere };
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const downstream = await input.execute(prepared.record);
|
|
252
|
+
const committed = mutate({ cwd }, () => {
|
|
253
|
+
const current = loadContinuation(prepared.record.continuation_key, cwd);
|
|
254
|
+
if (!current)
|
|
255
|
+
throw new Error('continuation_record_disappeared');
|
|
256
|
+
if (current.owner?.token !== prepared.record.owner?.token)
|
|
257
|
+
throw new Error('continuation_owner_fenced');
|
|
258
|
+
const record = { ...current, state: 'applied', downstream, owner: undefined, updated_at: nowISO() };
|
|
259
|
+
writeRecord(record, cwd);
|
|
260
|
+
audit(record, current.state, input.actor, input.actor_id, cwd);
|
|
261
|
+
return record;
|
|
262
|
+
});
|
|
263
|
+
return { record: committed, reused: prepared.reused };
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
mutate({ cwd }, () => {
|
|
267
|
+
const current = loadContinuation(prepared.record.continuation_key, cwd);
|
|
268
|
+
if (!current || current.owner?.token !== prepared.record.owner?.token)
|
|
269
|
+
return;
|
|
270
|
+
const record = { ...current, state: 'failed_recoverable', owner: undefined, last_error: error instanceof Error ? error.message : String(error), updated_at: nowISO() };
|
|
271
|
+
writeRecord(record, cwd);
|
|
272
|
+
audit(record, current.state, input.actor, input.actor_id, cwd);
|
|
273
|
+
});
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export function attachContinuationActionRequired(continuationId, actionId, actor, actorId, cwd) {
|
|
278
|
+
return mutate({ cwd }, () => {
|
|
279
|
+
const current = loadContinuation(continuationId, cwd);
|
|
280
|
+
if (!current)
|
|
281
|
+
throw new Error(`unknown continuation ${continuationId}`);
|
|
282
|
+
if (current.state !== 'approval_required')
|
|
283
|
+
throw new Error(`continuation ${continuationId} is ${current.state}, not approval_required`);
|
|
284
|
+
if (current.action_required_id && current.action_required_id !== actionId)
|
|
285
|
+
throw new Error('continuation_action_required_conflict');
|
|
286
|
+
const record = { ...current, action_required_id: actionId, updated_at: nowISO() };
|
|
287
|
+
writeRecord(record, cwd);
|
|
288
|
+
audit(record, current.state, actor, actorId, cwd);
|
|
289
|
+
return record;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
export function denyContinuation(continuationId, reason, actor, actorId, cwd) {
|
|
293
|
+
return mutate({ cwd }, () => {
|
|
294
|
+
const current = loadContinuation(continuationId, cwd);
|
|
295
|
+
if (!current)
|
|
296
|
+
throw new Error(`unknown continuation ${continuationId}`);
|
|
297
|
+
if (current.state === 'applied')
|
|
298
|
+
throw new Error(`continuation ${continuationId} is already applied`);
|
|
299
|
+
if (current.state === 'denied')
|
|
300
|
+
return current;
|
|
301
|
+
const record = {
|
|
302
|
+
...current,
|
|
303
|
+
decision: 'deny',
|
|
304
|
+
state: 'denied',
|
|
305
|
+
owner: undefined,
|
|
306
|
+
reason: [...current.reason, reason],
|
|
307
|
+
updated_at: nowISO(),
|
|
308
|
+
};
|
|
309
|
+
writeRecord(record, cwd);
|
|
310
|
+
audit(record, current.state, actor, actorId, cwd);
|
|
311
|
+
return record;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
export async function resumeApprovedContinuation(continuationId, actionId, actor, actorId, execute, cwd) {
|
|
315
|
+
const record = loadContinuation(continuationId, cwd);
|
|
316
|
+
if (!record)
|
|
317
|
+
throw new Error(`unknown continuation ${continuationId}`);
|
|
318
|
+
if (record.action_required_id !== actionId)
|
|
319
|
+
throw new Error('continuation_approval_mismatch');
|
|
320
|
+
const source = getLoop(record.source_loop_id, cwd);
|
|
321
|
+
const artifact = source?.artifacts.find((item) => item.artifact_id === record.source_artifact_id);
|
|
322
|
+
if (!source || !artifact)
|
|
323
|
+
throw new Error('continuation_source_missing');
|
|
324
|
+
mutate({ cwd }, () => {
|
|
325
|
+
const fresh = loadContinuation(continuationId, cwd);
|
|
326
|
+
if (fresh.state === 'applied')
|
|
327
|
+
return;
|
|
328
|
+
if (fresh.state !== 'approval_required' && fresh.state !== 'failed_recoverable')
|
|
329
|
+
throw new Error(`continuation ${continuationId} is ${fresh.state}`);
|
|
330
|
+
writeRecord({ ...fresh, state: 'failed_recoverable', autonomy_mode: 'autonomous', decision: 'auto', reason: [...fresh.reason, `approved by ${actor}`], updated_at: nowISO() }, cwd);
|
|
331
|
+
});
|
|
332
|
+
return ensureContinuation({
|
|
333
|
+
source_loop: source, source_artifact: artifact, action: record.action, action_index: record.action_index,
|
|
334
|
+
autonomy_mode: 'autonomous', risk: 'normal', actor, actor_id: actorId, execute,
|
|
335
|
+
}, cwd);
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=continuation.js.map
|
|
@@ -234,6 +234,19 @@ export const BclawLoopBindSchema = z.object({
|
|
|
234
234
|
// No expected_version: bind is idempotent by loop phase (past `bind` → noop), not CAS.
|
|
235
235
|
...CallerEnvelopeFields,
|
|
236
236
|
});
|
|
237
|
+
/**
|
|
238
|
+
* Evaluate and apply one persisted cross-loop continuation. This is an
|
|
239
|
+
* orchestration intent: the downstream mutation still traverses the public
|
|
240
|
+
* `open` and `bind` handlers, never a private Loop-store shortcut.
|
|
241
|
+
*/
|
|
242
|
+
export const BclawLoopContinueSchema = z.object({
|
|
243
|
+
intent: z.literal('continue'),
|
|
244
|
+
loop_id: z.string().regex(/^lop_[0-9a-z]+$/),
|
|
245
|
+
action_index: z.number().int().nonnegative().default(0),
|
|
246
|
+
autonomy_mode: z.enum(['autonomous', 'require_approval', 'deny']).default('autonomous'),
|
|
247
|
+
risk: z.enum(['normal', 'protected']).default('normal'),
|
|
248
|
+
...CallerEnvelopeFields,
|
|
249
|
+
});
|
|
237
250
|
/**
|
|
238
251
|
* pln#508 step 2 — `bclaw_loop(intent='request_input')`.
|
|
239
252
|
*
|
|
@@ -306,6 +319,7 @@ export const BclawLoopRequestSchema = z.discriminatedUnion('intent', [
|
|
|
306
319
|
BclawLoopCloseSchema,
|
|
307
320
|
BclawLoopVerifySchema,
|
|
308
321
|
BclawLoopBindSchema,
|
|
322
|
+
BclawLoopContinueSchema,
|
|
309
323
|
BclawLoopRequestInputSchema,
|
|
310
324
|
BclawLoopProvideInputSchema,
|
|
311
325
|
]);
|
|
@@ -323,6 +337,7 @@ export const BCLAW_LOOP_INTENTS = [
|
|
|
323
337
|
'close',
|
|
324
338
|
'verify',
|
|
325
339
|
'bind',
|
|
340
|
+
'continue',
|
|
326
341
|
'request_input',
|
|
327
342
|
'provide_input',
|
|
328
343
|
];
|
package/dist/core/loops/index.js
CHANGED
|
@@ -21,4 +21,5 @@ export { deriveWorkerReplyContract, renderWorkerReplyProse, workerReplyNextActio
|
|
|
21
21
|
export { abortAttempt, inspectAttempt, matchEvidence, prepareAttempt, projectAndCross, revokeAttempt, } from './attempt-authority.js';
|
|
22
22
|
export { LOOP_KIND_POLICIES, assertLoopKindPoliciesComplete, isWorkerPhase, phasePolicy, policyForKind, } from './kind-policies.js';
|
|
23
23
|
export { ensureTurnExecutionProjections, prepareTurnExecution, } from './turn-execution.js';
|
|
24
|
+
export { CONTINUATION_POLICY_VERSION, ContinuationDecisionSchema, ContinuationRecordSchema, ContinuationStateSchema, attachContinuationActionRequired, denyContinuation, ensureContinuation, evaluateContinuation, listContinuations, loadContinuation, resumeApprovedContinuation, } from './continuation.js';
|
|
24
25
|
//# sourceMappingURL=index.js.map
|
package/dist/core/loops/types.js
CHANGED
|
@@ -25,6 +25,12 @@ export const LoopLinksSchema = z.object({
|
|
|
25
25
|
sequence_ids: z.array(z.string().min(1)).optional(),
|
|
26
26
|
/** Upstream loop in an ideation → implementation → review pipeline. */
|
|
27
27
|
source_loop_id: z.string().regex(/^lop_[0-9a-z]+$/).optional(),
|
|
28
|
+
/** Exact upstream artifact that authorized this continuation. */
|
|
29
|
+
source_artifact_id: z.string().regex(/^art_[0-9a-z]+$/).optional(),
|
|
30
|
+
/** Sealed digest of source_artifact_id at continuation evaluation time. */
|
|
31
|
+
source_artifact_digest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
32
|
+
/** Durable, deterministic identity of the policy decision that created this loop. */
|
|
33
|
+
continuation_key: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
28
34
|
});
|
|
29
35
|
/**
|
|
30
36
|
* Memory categories a loop phase can request via `context_filter` (pln#492).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { listAgentIdentities } from './agent-registry.js';
|
|
2
|
+
import { resolveExecutionCandidate } from './execution-contract.js';
|
|
3
|
+
export const REVIEWER_SELECTION_POLICY_VERSION = 'reviewer-selection-v1';
|
|
4
|
+
/**
|
|
5
|
+
* Select a concrete review worker from project-registered identities.
|
|
6
|
+
*
|
|
7
|
+
* The shared execution-contract resolver supplies capability checks and stable
|
|
8
|
+
* ordering. The policy additionally enforces reviewer independence by
|
|
9
|
+
* excluding every identity frozen onto an implementation slot.
|
|
10
|
+
*/
|
|
11
|
+
export function selectImplementationReviewer(source, cwd) {
|
|
12
|
+
if (source.kind !== 'implementation') {
|
|
13
|
+
throw new Error(`reviewer_selection_source_invalid: loop ${source.id} is ${source.kind}`);
|
|
14
|
+
}
|
|
15
|
+
const excludedImplementers = source.slots
|
|
16
|
+
.filter((slot) => Boolean(slot.agent))
|
|
17
|
+
.map((slot) => ({ agent: slot.agent, ...(slot.agent_id ? { agent_id: slot.agent_id } : {}) }));
|
|
18
|
+
const excludedNames = new Set(excludedImplementers.map((identity) => identity.agent.normalize('NFC')));
|
|
19
|
+
const excludedIds = new Set(excludedImplementers.flatMap((identity) => identity.agent_id ? [identity.agent_id.normalize('NFC')] : []));
|
|
20
|
+
const identities = listAgentIdentities(cwd)
|
|
21
|
+
.filter((identity) => identity.kind !== 'human')
|
|
22
|
+
.filter((identity) => !excludedNames.has(identity.agent_name.normalize('NFC')) && !excludedIds.has(identity.agent_id.normalize('NFC')))
|
|
23
|
+
.map((identity) => ({ agent: identity.agent_name, agent_id: identity.agent_id }));
|
|
24
|
+
const resolution = resolveExecutionCandidate(identities, { roles: ['review'], required_surfaces: ['cli_spawn'], execution_surfaces: [], required_tools: [] });
|
|
25
|
+
if (resolution.kind !== 'selected') {
|
|
26
|
+
const reasons = resolution.evaluated
|
|
27
|
+
.map((candidate) => `${candidate.agent}:${candidate.snapshot.reasons.map((reason) => reason.code).join('+') || 'excluded'}`)
|
|
28
|
+
.join(', ');
|
|
29
|
+
throw new Error(`continuation_reviewer_unavailable: no independent spawnable reviewer${reasons ? ` (${reasons})` : ''}`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
policy_version: REVIEWER_SELECTION_POLICY_VERSION,
|
|
33
|
+
agent: resolution.selected.agent,
|
|
34
|
+
agent_id: resolution.selected.agent_id,
|
|
35
|
+
evaluated: resolution.evaluated,
|
|
36
|
+
excluded_implementers: excludedImplementers,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=reviewer-policy.js.map
|
package/dist/core/schema.js
CHANGED
|
@@ -923,11 +923,18 @@ export const ActionRequiredResponseSchema = z.object({
|
|
|
923
923
|
responded_by_id: z.string().optional(),
|
|
924
924
|
responded_at: z.string(),
|
|
925
925
|
});
|
|
926
|
+
export const ActionRequiredTargetSchema = z.discriminatedUnion('kind', [
|
|
927
|
+
z.object({ kind: z.literal('assignment'), assignment_id: z.string() }),
|
|
928
|
+
z.object({ kind: z.literal('continuation'), continuation_id: z.string().regex(/^ctn_[a-f0-9]{24}$/) }),
|
|
929
|
+
]);
|
|
926
930
|
export const ActionRequiredSchema = z.object({
|
|
927
931
|
schema_version: z.number().int().positive().optional(),
|
|
928
932
|
id: z.string(),
|
|
929
933
|
short_label: z.string().optional(),
|
|
930
|
-
|
|
934
|
+
/** Legacy top-level assignment link; retained for v1 records. */
|
|
935
|
+
assignment_id: z.string().optional(),
|
|
936
|
+
/** Discriminated approval target. New records always persist this field. */
|
|
937
|
+
target: ActionRequiredTargetSchema.optional(),
|
|
931
938
|
run_id: z.string().optional(),
|
|
932
939
|
claim_id: z.string().optional(),
|
|
933
940
|
message_id: z.string().optional(),
|
|
@@ -949,6 +956,14 @@ export const ActionRequiredSchema = z.object({
|
|
|
949
956
|
resolved_at: z.string().optional(),
|
|
950
957
|
response: ActionRequiredResponseSchema.optional(),
|
|
951
958
|
tags: TagsWithDefaultSchema,
|
|
959
|
+
}).superRefine((action, ctx) => {
|
|
960
|
+
const target = action.target ?? (action.assignment_id ? { kind: 'assignment', assignment_id: action.assignment_id } : undefined);
|
|
961
|
+
if (!target) {
|
|
962
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['target'], message: 'ActionRequired requires an assignment or continuation target' });
|
|
963
|
+
}
|
|
964
|
+
if (target?.kind === 'assignment' && action.assignment_id && target.assignment_id !== action.assignment_id) {
|
|
965
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['target'], message: 'assignment target must match assignment_id' });
|
|
966
|
+
}
|
|
952
967
|
});
|
|
953
968
|
// --- Runtime notes schemas ---
|
|
954
969
|
export const RuntimeNoteTypeSchema = z.enum(['observation', 'session_start', 'session_end']);
|
package/dist/facts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
|
|
2
|
-
// Source: brainclaw v1.28.
|
|
2
|
+
// Source: brainclaw v1.28.1 on 2026-08-24T17:40:07.204Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.28.
|
|
5
|
-
"generated_at": "2026-08-
|
|
4
|
+
"version": "1.28.1",
|
|
5
|
+
"generated_at": "2026-08-24T17:40:07.204Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 70,
|
|
8
8
|
"published_count": 68,
|
|
@@ -478,7 +478,7 @@ export const FACTS = {
|
|
|
478
478
|
},
|
|
479
479
|
"bench": {
|
|
480
480
|
"schema": "brainclaw.bench.v1",
|
|
481
|
-
"generated_at": "2026-08-
|
|
481
|
+
"generated_at": "2026-08-24T17:40:05.036Z",
|
|
482
482
|
"node_version": "v24.19.0",
|
|
483
483
|
"platform": "linux-x64",
|
|
484
484
|
"repeats": 3,
|
|
@@ -487,7 +487,7 @@ export const FACTS = {
|
|
|
487
487
|
"name": "cold_onboard",
|
|
488
488
|
"volume": "empty",
|
|
489
489
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
490
|
-
"duration_ms_median":
|
|
490
|
+
"duration_ms_median": 85,
|
|
491
491
|
"payload_chars_median": 1640,
|
|
492
492
|
"payload_tokens_est_median": 410
|
|
493
493
|
},
|
|
@@ -495,7 +495,7 @@ export const FACTS = {
|
|
|
495
495
|
"name": "warm_work",
|
|
496
496
|
"volume": "medium",
|
|
497
497
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
498
|
-
"duration_ms_median":
|
|
498
|
+
"duration_ms_median": 128,
|
|
499
499
|
"payload_chars_median": 2626,
|
|
500
500
|
"payload_tokens_est_median": 657
|
|
501
501
|
},
|
|
@@ -503,7 +503,7 @@ export const FACTS = {
|
|
|
503
503
|
"name": "first_edit",
|
|
504
504
|
"volume": "medium",
|
|
505
505
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
506
|
-
"duration_ms_median":
|
|
506
|
+
"duration_ms_median": 11,
|
|
507
507
|
"payload_chars_median": 1305,
|
|
508
508
|
"payload_tokens_est_median": 326
|
|
509
509
|
}
|
package/dist/facts.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.28.
|
|
3
|
-
"generated_at": "2026-08-
|
|
2
|
+
"version": "1.28.1",
|
|
3
|
+
"generated_at": "2026-08-24T17:40:07.204Z",
|
|
4
4
|
"tools": {
|
|
5
5
|
"count": 70,
|
|
6
6
|
"published_count": 68,
|
|
@@ -476,7 +476,7 @@
|
|
|
476
476
|
},
|
|
477
477
|
"bench": {
|
|
478
478
|
"schema": "brainclaw.bench.v1",
|
|
479
|
-
"generated_at": "2026-08-
|
|
479
|
+
"generated_at": "2026-08-24T17:40:05.036Z",
|
|
480
480
|
"node_version": "v24.19.0",
|
|
481
481
|
"platform": "linux-x64",
|
|
482
482
|
"repeats": 3,
|
|
@@ -485,7 +485,7 @@
|
|
|
485
485
|
"name": "cold_onboard",
|
|
486
486
|
"volume": "empty",
|
|
487
487
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
488
|
-
"duration_ms_median":
|
|
488
|
+
"duration_ms_median": 85,
|
|
489
489
|
"payload_chars_median": 1640,
|
|
490
490
|
"payload_tokens_est_median": 410
|
|
491
491
|
},
|
|
@@ -493,7 +493,7 @@
|
|
|
493
493
|
"name": "warm_work",
|
|
494
494
|
"volume": "medium",
|
|
495
495
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
496
|
-
"duration_ms_median":
|
|
496
|
+
"duration_ms_median": 128,
|
|
497
497
|
"payload_chars_median": 2626,
|
|
498
498
|
"payload_tokens_est_median": 657
|
|
499
499
|
},
|
|
@@ -501,7 +501,7 @@
|
|
|
501
501
|
"name": "first_edit",
|
|
502
502
|
"volume": "medium",
|
|
503
503
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
504
|
-
"duration_ms_median":
|
|
504
|
+
"duration_ms_median": 11,
|
|
505
505
|
"payload_chars_median": 1305,
|
|
506
506
|
"payload_tokens_est_median": 326
|
|
507
507
|
}
|
package/docs/cli.md
CHANGED
|
@@ -1016,9 +1016,11 @@ research, and debug; they are not a review-only command group.
|
|
|
1016
1016
|
| `takeover <loop_id>` | slot, turn, expected epoch, cause, liveness evidence, external-effect policy, next workspace and coordinator identity | Fence one physical generation and arm a successor without changing the logical Assignment. |
|
|
1017
1017
|
| `advance <loop_id>` | — | Advance through the protocol; optional `--to-phase`, `--force`, `--reason`. |
|
|
1018
1018
|
| `add-artifact <loop_id>` | `--phase --type --body` | Attach a typed artifact; optional producer and ref. |
|
|
1019
|
+
| `continue <loop_id>` | — | Evaluate an attested Ideation→Implementation or Implementation→Review action and persist/apply `AUTO`, `REQUIRE_APPROVAL`, or `DENY`; options: `--action-index`, `--autonomy-mode`, `--risk`. Review continuation selects an independent registered reviewer and fails closed if none is available. |
|
|
1019
1020
|
|
|
1020
1021
|
```bash
|
|
1021
1022
|
brainclaw loop advance lop_abc --json
|
|
1023
|
+
brainclaw loop continue lop_abc --autonomy-mode autonomous --risk normal --json
|
|
1022
1024
|
brainclaw loop takeover lop_abc \
|
|
1023
1025
|
--slot lsl_abc --turn-id tat_abc --expected-epoch 0 \
|
|
1024
1026
|
--cause "worker is no longer live" \
|
|
@@ -1028,7 +1030,7 @@ brainclaw loop takeover lop_abc \
|
|
|
1028
1030
|
```
|
|
1029
1031
|
|
|
1030
1032
|
The full public lifecycle (`open`, `get`, `list`, `pause`, `resume`, `close`,
|
|
1031
|
-
`bind`, `verify`, `request_input`, `provide_input`, and the verbs above) is the
|
|
1033
|
+
`bind`, `verify`, `continue`, `request_input`, `provide_input`, and the verbs above) is the
|
|
1032
1034
|
MCP `bclaw_loop(intent)` facade. Direct MCP `open` requires
|
|
1033
1035
|
`allow_orphan=true`; review and ideation normally start through
|
|
1034
1036
|
`bclaw_coordinate` so opening and dispatch stay one operation. See the
|
|
@@ -2032,7 +2034,7 @@ The default catalog is intentionally small and centred on the canonical grammar.
|
|
|
2032
2034
|
|---|---|
|
|
2033
2035
|
| `bclaw_coordinate(intent)` | Assign, consult, review, reroute, or summarize across agents. Pass `open_loop: true` on `intent="review"` to also dispatch the reviewer turn. |
|
|
2034
2036
|
| `bclaw_dispatch(intent)` | Parallelize execute across a sequence's lanes (analysis / execute / review). |
|
|
2035
|
-
| `bclaw_loop(intent)` | Open, inspect, or drive a multi-turn loop.
|
|
2037
|
+
| `bclaw_loop(intent)` | Open, inspect, or drive a multi-turn loop. `continue` persists and applies policy-governed cross-loop progression through the public mutation path. Implementation loops add engine-only `bind` and `verify`; any kind may use `request_input` / `provide_input`. Trusted `turn(dispatch=true)` remains the only worker launch path. A direct `open` must include `allow_orphan: true`. |
|
|
2036
2038
|
|
|
2037
2039
|
**Sequences**:
|
|
2038
2040
|
|
|
@@ -363,6 +363,7 @@ Additional shared and engine-owned actions complete the lifecycle:
|
|
|
363
363
|
- **request_input** / **provide_input** — bounded, evidence-backed operator clarification usable by any protocol.
|
|
364
364
|
- **bind** — implementation-loop engine action that validates the linked sequence and advances to `execute`; it never launches a worker.
|
|
365
365
|
- **verify** — implementation/debug engine action that runs the opener-configured command outside the loop lock, then records a verification-attested report.
|
|
366
|
+
- **continue** — orchestration action above the Loop engine. It evaluates an attested `next_action`, persists `AUTO | REQUIRE_APPROVAL | DENY`, and applies supported actions through the same public `open`/`bind` handlers.
|
|
366
367
|
|
|
367
368
|
Artifact authority is sealed at these verb boundaries. `produced_by` is
|
|
368
369
|
derived from the authenticated slot/engine/coordinator context. A narrative
|
|
@@ -396,6 +397,7 @@ type BclawLoopInput = BclawLoopCallerEnvelope & (
|
|
|
396
397
|
| { intent: 'close'; loop_id: LoopId; status: 'completed' | 'cancelled' | 'blocked'; reason?: string; expected_version?: number }
|
|
397
398
|
| { intent: 'verify'; loop_id: LoopId }
|
|
398
399
|
| { intent: 'bind'; loop_id: LoopId; dry_run?: boolean; lanes?: string[]; auto_execute?: boolean; model?: string; max_assignments?: number }
|
|
400
|
+
| { intent: 'continue'; loop_id: LoopId; action_index?: number; autonomy_mode?: 'autonomous' | 'require_approval' | 'deny'; risk?: 'normal' | 'protected' }
|
|
399
401
|
| { intent: 'request_input'; loop_id: LoopId; slot_id: SlotId; phase: string; question_text: string; evidence: string[]; suggested_default?: string; options?: OperatorQuestionOption[]; pause_scope: 'slot' | 'loop'; on_timeout: 'use_default' | 'cancel_loop' | 'continue_incomplete'; timeout_at?: string; expected_version?: number }
|
|
400
402
|
| { intent: 'provide_input'; loop_id: LoopId; replies_to: string; resolved_via: 'answer' | 'choose' | 'skip' | 'timeout_default'; answer_text?: string; chosen_option_id?: string; by?: 'operator' | 'system'; expected_version?: number }
|
|
401
403
|
| { intent: 'get'; loop_id: LoopId; include_events?: boolean }
|
|
@@ -470,6 +472,29 @@ The shared lifecycle verbs are `turn`, `complete_turn`, `advance`,
|
|
|
470
472
|
use engine-only `bind` to validate their linked sequence and enter `execute`,
|
|
471
473
|
then `turn(dispatch:true)` for worker slots; `verify` runs their declared command.
|
|
472
474
|
|
|
475
|
+
### Persisted continuation authority
|
|
476
|
+
|
|
477
|
+
An accepted ideation synthesis and an attested implementation handoff no
|
|
478
|
+
longer expose ungoverned downstream mutations. Their `next_actions` point to
|
|
479
|
+
`bclaw_loop(intent="continue")`. The
|
|
480
|
+
continuation record binds the source loop, iteration, sealed artifact digest,
|
|
481
|
+
canonical action hash and policy version into a deterministic key. It is
|
|
482
|
+
written before the downstream mutation.
|
|
483
|
+
|
|
484
|
+
For Ideation→Implementation, `AUTO` invokes the ordinary public `open` handler
|
|
485
|
+
with that key in `linked.continuation_key`, then invokes engine-only `bind`.
|
|
486
|
+
For Implementation→Review, it deterministically selects a project-registered,
|
|
487
|
+
spawnable review-capable identity that did not occupy an implementation slot,
|
|
488
|
+
then invokes the ordinary public `bclaw_coordinate(intent="review",
|
|
489
|
+
open_loop=true)` path. If no independent reviewer exists, it fails closed.
|
|
490
|
+
A retry first scans existing loops for the key, so a crash after either public
|
|
491
|
+
mutation but before the response reuses the same loop. A live concurrent owner
|
|
492
|
+
is observed rather than stolen.
|
|
493
|
+
`REQUIRE_APPROVAL` creates an `ActionRequired` whose discriminated target is
|
|
494
|
+
the continuation; approval resumes the same record, while rejection or expiry
|
|
495
|
+
persists `DENY`. Unsupported actions, placeholders, missing evidence and
|
|
496
|
+
ambiguous downstreams fail closed.
|
|
497
|
+
|
|
473
498
|
### Clarification is a cross-cutting primitive
|
|
474
499
|
|
|
475
500
|
Clarification is deliberately not a sixth protocol. Any workflow can call
|
|
@@ -408,7 +408,12 @@ will still succeed. A follow-up PR will strip the dead handler code.
|
|
|
408
408
|
changelog records the published MCP surface fingerprint. When a tool
|
|
409
409
|
name, tier, category, or input schema changes, the test fails until
|
|
410
410
|
this section is updated.
|
|
411
|
-
- MCP public surface fingerprint: `sha256:
|
|
411
|
+
- MCP public surface fingerprint: `sha256:be86e5571fcd0226`
|
|
412
|
+
(updated 2026-08-24 for persisted continuation authority: additive
|
|
413
|
+
`bclaw_loop(intent="continue")` inputs `action_index`, `autonomy_mode`, and
|
|
414
|
+
`risk`; the intent evaluates an attested Ideation→Implementation action,
|
|
415
|
+
persists AUTO/REQUIRE_APPROVAL/DENY, and reuses the public open/bind path.)
|
|
416
|
+
Previous: `sha256:681c47cba85b79c3`
|
|
412
417
|
(`LoopSlotInput` gains optional `lane`, `scope_hint`, `plan_ids`, and
|
|
413
418
|
`step_ids` fields so implementation-loop lane scope and provenance survive
|
|
414
419
|
through the public facade. Existing callers remain valid.)
|