brainclaw 1.28.0 → 1.28.2
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/code-map.js +2 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/harvest.js +32 -43
- package/dist/commands/loop.js +12 -0
- package/dist/commands/loops-handlers.js +284 -17
- package/dist/commands/mcp-catalog.js +6 -3
- package/dist/commands/mcp-write-claims.js +55 -8
- package/dist/commands/mcp-write-coordination.js +413 -137
- package/dist/commands/mcp.js +32 -4
- package/dist/core/actions.js +17 -3
- package/dist/core/agentrun-reconciler.js +138 -4
- package/dist/core/claims.js +4 -1
- package/dist/core/code-map/backend.js +8 -0
- package/dist/core/execution-adapters.js +15 -7
- package/dist/core/hygiene-policy.js +2 -1
- package/dist/core/loop-turn-dispatch.js +18 -1
- package/dist/core/loops/attempt-authority.js +22 -4
- package/dist/core/loops/attempt-generations.js +17 -4
- package/dist/core/loops/attempt-reservation.js +14 -1
- package/dist/core/loops/attempt-takeover.js +173 -76
- 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/reconcile-turn.js +224 -26
- package/dist/core/loops/result-reducers.js +8 -8
- package/dist/core/loops/turn-execution.js +38 -19
- package/dist/core/loops/types.js +9 -0
- package/dist/core/loops/verbs.js +1 -1
- package/dist/core/reviewer-policy.js +39 -0
- package/dist/core/schema.js +16 -1
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/cli.md +4 -2
- package/docs/code-map.md +10 -0
- package/docs/concepts/loop-engine.md +30 -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')
|
|
@@ -108,6 +108,8 @@ function printStatus(status, options) {
|
|
|
108
108
|
}
|
|
109
109
|
console.log('Code Map status');
|
|
110
110
|
console.log(` Store: ${status.store_exists ? 'present' : 'absent'}`);
|
|
111
|
+
console.log(` Root: ${status.resolution.project_root}`);
|
|
112
|
+
console.log(` Path: ${status.resolution.store_path}`);
|
|
111
113
|
console.log(` ${badgeLine(status.freshness_badge)}`);
|
|
112
114
|
if (status.stats) {
|
|
113
115
|
console.log(` Files: ${status.stats.files_indexed}`);
|
package/dist/commands/doctor.js
CHANGED
package/dist/commands/harvest.js
CHANGED
|
@@ -25,54 +25,43 @@ import { commitWorktreeOnBehalf, worktreesBaseDir, resolveGitToplevel } from '..
|
|
|
25
25
|
import { closeReviewLoopFromLaneResult } from '../core/review-loop-close.js';
|
|
26
26
|
import { closeIdeationLoopFromLaneResult } from '../core/ideation-loop-close.js';
|
|
27
27
|
import { dispatchReviewLoopTurn, turnOwnedLoopEnabled } from '../core/review-loop-turn-dispatch.js';
|
|
28
|
-
import {
|
|
29
|
-
import { findReservationByAssignmentId } from '../core/loops/attempt-reservation.js';
|
|
30
|
-
import { resolveTurnGenerationChain } from '../core/loops/attempt-generations.js';
|
|
28
|
+
import { reconcileTurnOwnedLane, turnOwnedLaneEvidence } from '../core/loops/reconcile-turn.js';
|
|
31
29
|
import { getLoop } from '../core/loops/store.js';
|
|
32
30
|
import { phasePolicy } from '../core/loops/kind-policies.js';
|
|
33
|
-
import { readCompletionSignals } from '../core/runtime-signals.js';
|
|
34
31
|
import { reconcileClaimConformity } from '../core/claim-conformity.js';
|
|
35
32
|
import { toWarningDetail } from '../core/warnings.js';
|
|
36
33
|
import { harvestHarnessObservation } from '../core/harness-adapters/index.js';
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
&& reservation.phase === 'critique'
|
|
69
|
-
&& lane.artifact_type === 'critique'
|
|
70
|
-
&& (lane.body ?? '').trim().length > 0
|
|
71
|
-
? [{ body: lane.body.trim() }]
|
|
72
|
-
: undefined;
|
|
73
|
-
const result = reconcileTurn({ turn_id: reservation.turn_id, lane: enrichedLane, cwd, critiques });
|
|
74
|
-
return { reservation, result };
|
|
75
|
-
}
|
|
34
|
+
/**
|
|
35
|
+
* pln#630 PR3a — finalize a TURN-OWNED review lane via the exactly-once `reconcileTurn`
|
|
36
|
+
* instead of the legacy `closeReviewLoopFromLaneResult`. Returns `undefined` for a legacy
|
|
37
|
+
* (non-reserved) lane so the caller runs the unchanged legacy path — this is the
|
|
38
|
+
* exactly-one-finalizer discriminator: a lane is turn-owned iff a reservation OWNS its
|
|
39
|
+
* assignment_id (only the turn-owned dispatch writes a reservation file).
|
|
40
|
+
*
|
|
41
|
+
* Evidence sourcing (the load-bearing subtlety): a real reviewer's LANE-RESULT.json is
|
|
42
|
+
* KEYLESS — the review brief never asks the worker to echo turn_id/run_id/nonce — so
|
|
43
|
+
* read-strict `reconcileTurn` (which matches lane.{turn_id,run_id,nonce} against the
|
|
44
|
+
* attempt) would REJECT it. We source the keys authoritatively: turn_id + run_id are
|
|
45
|
+
* deterministic from the reservation, and the NONCE — the non-derivable proof that THIS
|
|
46
|
+
* launch generation actually ran — comes from the coordinator's completion SENTINEL
|
|
47
|
+
* (written mechanically by the ack-wrapper with the launch-grant token). A caller/test
|
|
48
|
+
* that already supplies keyed lanes is honored (lane.* wins); a stale generation's
|
|
49
|
+
* sentinel carries the old token → still rejected, preserving the anti-stale guarantee.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* The turn-owned FINALIZATION discriminator (pln#630, review Finding 1). A lane finalizes via
|
|
53
|
+
* the exactly-once reconcileTurn ONLY if a committed reservation OWNS it AND turn-keyed evidence
|
|
54
|
+
* (the nonce) is available — from the lane or the coordinator's completion SENTINEL. Without the
|
|
55
|
+
* nonce, reconcileTurn's read-strict gate can NEVER converge: this is reachable in production
|
|
56
|
+
* when a turn-owned dispatch WON the fence but did not ack-wrap-spawn (inbox_only / IDE-only
|
|
57
|
+
* reviewer, command_ready_manual, capacity cap, BRAINCLAW_NO_SPAWN, worktree-creation failure) —
|
|
58
|
+
* it minted a reservation but no sentinel will ever be written. Returning undefined there routes
|
|
59
|
+
* the lane to the LEGACY presence-based closer so the loop still converges instead of stalling
|
|
60
|
+
* forever. This is SAFE: the exactly-once SPAWN guarantee is enforced at DISPATCH by the launch
|
|
61
|
+
* fence (already run), so using legacy FINALIZATION for a sentinel-less lane reintroduces no
|
|
62
|
+
* double-spawn; and a sentinel that lands after a legacy close makes a later reconcile a
|
|
63
|
+
* terminal-loop idempotent no-op.
|
|
64
|
+
*/
|
|
76
65
|
/**
|
|
77
66
|
* Map a `reconcileTurn` result onto the `ReviewLoopCloseResult` shape harvest records for
|
|
78
67
|
* observability (entry.review_loop / CLI). No keep_claim / next_turn: the request_changes
|
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, deriveWorkerReplyContract, evaluatePhaseAdvanceGate, } 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,29 +62,86 @@ 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 [];
|
|
84
143
|
}
|
|
85
|
-
function errorResponse(intent, code, message, durationMs, result = null) {
|
|
144
|
+
function errorResponse(intent, code, message, durationMs, result = null, nextActions = []) {
|
|
86
145
|
return {
|
|
87
146
|
response: {
|
|
88
147
|
status: 'error',
|
|
@@ -93,10 +152,72 @@ function errorResponse(intent, code, message, durationMs, result = null) {
|
|
|
93
152
|
warnings: [],
|
|
94
153
|
error: `${code}: ${message}`,
|
|
95
154
|
duration_ms: durationMs,
|
|
155
|
+
...(nextActions.length > 0 ? { next_actions: nextActions } : {}),
|
|
96
156
|
},
|
|
97
157
|
summary: `✘ bclaw_loop[${intent}] ${code}: ${message}`,
|
|
98
158
|
};
|
|
99
159
|
}
|
|
160
|
+
function continuationUnavailableDiagnostic(loop, cwd) {
|
|
161
|
+
const phase = loop.phases.find((candidate) => candidate.name === loop.current_phase);
|
|
162
|
+
const gate = phase?.advance_gate;
|
|
163
|
+
const gateOutcome = evaluatePhaseAdvanceGate(loop, gate, cwd);
|
|
164
|
+
const contract = deriveWorkerReplyContract(loop);
|
|
165
|
+
const blockers = [];
|
|
166
|
+
const probableCauses = [];
|
|
167
|
+
const nextActions = [];
|
|
168
|
+
if (!gateOutcome.advance && gateOutcome.gate_reason)
|
|
169
|
+
blockers.push(gateOutcome.gate_reason);
|
|
170
|
+
const assigned = loop.slots.filter((slot) => slot.status === 'assigned' && slot.assignment_id);
|
|
171
|
+
const open = loop.slots.filter((slot) => slot.status === 'open'
|
|
172
|
+
&& !(loop.kind === 'ideation' && loop.current_phase === 'critique' && slot.role === 'champion'));
|
|
173
|
+
if (assigned.length > 0) {
|
|
174
|
+
probableCauses.push('one or more dispatched worker results have not converged into gate evidence');
|
|
175
|
+
for (const slot of assigned) {
|
|
176
|
+
nextActions.push({
|
|
177
|
+
tool: 'bclaw_find',
|
|
178
|
+
args: { entity: 'agent_run', filter: { assignment_id: slot.assignment_id, limit: 10 } },
|
|
179
|
+
when: `reconcile and inspect the AgentRun projection for slot ${slot.slot_id}`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const slot of open) {
|
|
184
|
+
probableCauses.push(`slot ${slot.slot_id} has not been dispatched`);
|
|
185
|
+
nextActions.push({
|
|
186
|
+
tool: 'bclaw_loop',
|
|
187
|
+
args: { intent: 'turn', loop_id: loop.id, slot_id: slot.slot_id, input: loop.goal ?? loop.title, dispatch: true },
|
|
188
|
+
when: `dispatch open slot ${slot.slot_id}`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (loop.kind === 'ideation') {
|
|
192
|
+
const draft = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'plan_draft');
|
|
193
|
+
if (!draft)
|
|
194
|
+
blockers.push('no attested plan_draft artifact is available for continuation');
|
|
195
|
+
if ((loop.linked?.plan_ids?.length ?? 0) === 0)
|
|
196
|
+
blockers.push('the source loop is not linked to a plan');
|
|
197
|
+
if ((loop.linked?.sequence_ids?.length ?? 0) !== 1)
|
|
198
|
+
blockers.push('the source loop must link exactly one implementation sequence');
|
|
199
|
+
}
|
|
200
|
+
else if (loop.kind === 'implementation') {
|
|
201
|
+
const handoff = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'handoff' && artifact.ref);
|
|
202
|
+
if (!handoff)
|
|
203
|
+
blockers.push('no attested handoff with a reviewable ref is available');
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
result: {
|
|
207
|
+
loop_id: loop.id,
|
|
208
|
+
phase: loop.current_phase,
|
|
209
|
+
gate: {
|
|
210
|
+
expected: contract?.requirements ?? (gate ? [gate] : []),
|
|
211
|
+
observed: gateOutcome.gate_reason ?? 'gate satisfied or no phase gate',
|
|
212
|
+
passed: gateOutcome.advance,
|
|
213
|
+
},
|
|
214
|
+
blockers: [...new Set(blockers)],
|
|
215
|
+
probable_causes: [...new Set(probableCauses)],
|
|
216
|
+
next_actions: nextActions,
|
|
217
|
+
},
|
|
218
|
+
next_actions: nextActions,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
100
221
|
function inferIntent(args) {
|
|
101
222
|
if (!args || typeof args !== 'object')
|
|
102
223
|
return 'unknown';
|
|
@@ -235,6 +356,59 @@ function trySweepLoopTimeouts(loop_id, cwd) {
|
|
|
235
356
|
}
|
|
236
357
|
catch { /* best-effort: never block facade on sweep errors */ }
|
|
237
358
|
}
|
|
359
|
+
/** Execute a persisted continuation through the same public handler used by MCP/CLI callers. */
|
|
360
|
+
export async function executeContinuationPublicAction(record, options) {
|
|
361
|
+
const args = record.action.args ?? {};
|
|
362
|
+
const linked = (args.linked && typeof args.linked === 'object' ? args.linked : {});
|
|
363
|
+
const publicArgs = {
|
|
364
|
+
...args,
|
|
365
|
+
linked: { ...linked, continuation_key: record.continuation_key },
|
|
366
|
+
client_request_id: `ctn_${record.continuation_key}`,
|
|
367
|
+
agent: options.actor,
|
|
368
|
+
agentId: options.agentId,
|
|
369
|
+
};
|
|
370
|
+
let downstreamId;
|
|
371
|
+
if (record.action.tool === 'bclaw_loop') {
|
|
372
|
+
const opened = await handleBclawLoop({
|
|
373
|
+
args: publicArgs, cwd: options.cwd, defaultActor: options.actor, sessionId: options.sessionId,
|
|
374
|
+
});
|
|
375
|
+
if (opened.response.status !== 'ok')
|
|
376
|
+
throw new Error(opened.response.error ?? opened.summary);
|
|
377
|
+
downstreamId = opened.response.result.loop?.id;
|
|
378
|
+
}
|
|
379
|
+
else if (record.action.tool === 'bclaw_coordinate') {
|
|
380
|
+
const coordinateCwd = options.cwd ?? process.cwd();
|
|
381
|
+
const coordinated = await handleBclawCoordinate(publicArgs, {
|
|
382
|
+
cwd: coordinateCwd,
|
|
383
|
+
connectionSessionId: options.sessionId,
|
|
384
|
+
// The persisted source loop is an explicit store selector. Preserve that
|
|
385
|
+
// provenance so a multi-project workspace cannot reinterpret this as a
|
|
386
|
+
// bare-cwd review and reject or misroute the downstream loop.
|
|
387
|
+
effectiveScope: {
|
|
388
|
+
cwd: coordinateCwd,
|
|
389
|
+
active_source: 'explicit',
|
|
390
|
+
resolved_project: { path: coordinateCwd },
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
if (coordinated.response.isError) {
|
|
394
|
+
const details = coordinated.response.structuredContent;
|
|
395
|
+
throw new Error(details?.error ?? details?.message ?? 'continuation_coordinate_failed');
|
|
396
|
+
}
|
|
397
|
+
const facade = coordinated.response.structuredContent;
|
|
398
|
+
if (facade?.status === 'error')
|
|
399
|
+
throw new Error(facade.error ?? 'continuation_coordinate_failed');
|
|
400
|
+
downstreamId = facade?.result?.loop_id;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
throw new Error(`continuation_action_unsupported: ${record.action.tool}`);
|
|
404
|
+
}
|
|
405
|
+
if (!downstreamId)
|
|
406
|
+
throw new Error('continuation_open_missing_loop');
|
|
407
|
+
if (process.env.BRAINCLAW_TEST_FAULT_CONTINUATION_AFTER_OPEN === '1') {
|
|
408
|
+
throw new Error('fault_injection: continuation_after_open');
|
|
409
|
+
}
|
|
410
|
+
return { kind: 'loop', id: downstreamId };
|
|
411
|
+
}
|
|
238
412
|
export async function handleBclawLoop(options) {
|
|
239
413
|
const startMs = Date.now();
|
|
240
414
|
const defaultActor = options.defaultActor ?? 'bclaw_loop';
|
|
@@ -592,6 +766,99 @@ export async function handleBclawLoop(options) {
|
|
|
592
766
|
next_expected: computeNextExpected(result.thread),
|
|
593
767
|
}, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summary);
|
|
594
768
|
}
|
|
769
|
+
case 'continue': {
|
|
770
|
+
const source = getLoop(req.loop_id, options.cwd);
|
|
771
|
+
if (!source) {
|
|
772
|
+
return errorResponse('continue', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
773
|
+
}
|
|
774
|
+
const actions = proposedPipelineActions(source, options.cwd);
|
|
775
|
+
const action = actions[req.action_index];
|
|
776
|
+
if (!action) {
|
|
777
|
+
const diagnostic = continuationUnavailableDiagnostic(source, options.cwd);
|
|
778
|
+
return errorResponse('continue', 'continuation_unavailable', `no executable continuation action ${req.action_index} for ${source.id}; inspect gate/blockers and execute the supplied recovery actions`, Date.now() - startMs, diagnostic.result, diagnostic.next_actions);
|
|
779
|
+
}
|
|
780
|
+
const sourceArtifactId = action.args?.linked?.source_artifact_id;
|
|
781
|
+
const sourceArtifact = source.artifacts.find((artifact) => artifact.artifact_id === sourceArtifactId);
|
|
782
|
+
if (!sourceArtifact) {
|
|
783
|
+
return errorResponse('continue', 'continuation_source_missing', 'source continuation artifact disappeared', Date.now() - startMs);
|
|
784
|
+
}
|
|
785
|
+
const ensured = await ensureContinuation({
|
|
786
|
+
source_loop: source,
|
|
787
|
+
source_artifact: sourceArtifact,
|
|
788
|
+
action,
|
|
789
|
+
action_index: req.action_index,
|
|
790
|
+
autonomy_mode: req.autonomy_mode,
|
|
791
|
+
risk: req.risk,
|
|
792
|
+
actor,
|
|
793
|
+
actor_id: agentId,
|
|
794
|
+
execute: (record) => executeContinuationPublicAction(record, {
|
|
795
|
+
cwd: options.cwd, actor, agentId, sessionId: options.sessionId,
|
|
796
|
+
}),
|
|
797
|
+
}, options.cwd);
|
|
798
|
+
let continuation = ensured.record;
|
|
799
|
+
if (continuation.state === 'approval_required') {
|
|
800
|
+
let approval = continuation.action_required_id
|
|
801
|
+
? loadActionRequired(continuation.action_required_id, options.cwd)
|
|
802
|
+
: undefined;
|
|
803
|
+
if (!approval) {
|
|
804
|
+
approval = createActionRequired({
|
|
805
|
+
target: { kind: 'continuation', continuation_id: continuation.id },
|
|
806
|
+
plan_id: source.linked?.plan_ids?.[0],
|
|
807
|
+
sequence_id: source.linked?.sequence_ids?.[0],
|
|
808
|
+
agent: actor,
|
|
809
|
+
agent_id: agentId,
|
|
810
|
+
session_id: options.sessionId,
|
|
811
|
+
kind: 'plan_approval',
|
|
812
|
+
scope: source.goal,
|
|
813
|
+
title: `Approve continuation from ${source.id}`,
|
|
814
|
+
prompt: continuation.reason.join('; '),
|
|
815
|
+
tags: ['loop-engine', 'continuation', 'approval-required'],
|
|
816
|
+
}, options.cwd);
|
|
817
|
+
continuation = attachContinuationActionRequired(continuation.id, approval.id, actor, agentId, options.cwd);
|
|
818
|
+
}
|
|
819
|
+
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}`);
|
|
820
|
+
handled.response.next_actions = [{
|
|
821
|
+
tool: 'bclaw_assignment_action',
|
|
822
|
+
args: { action_id: approval.id, outcome: 'resolved' },
|
|
823
|
+
when: 'a different trusted supervisor approves this continuation',
|
|
824
|
+
}];
|
|
825
|
+
return handled;
|
|
826
|
+
}
|
|
827
|
+
if (continuation.state === 'denied') {
|
|
828
|
+
return successResponse('continue', { continuation }, [{ type: 'continuation', id: continuation.id }], [], [], Date.now() - startMs, `continuation ${continuation.id} denied: ${continuation.reason.join('; ')}`);
|
|
829
|
+
}
|
|
830
|
+
if (ensured.executing_elsewhere) {
|
|
831
|
+
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`);
|
|
832
|
+
handled.response.next_actions = [{
|
|
833
|
+
tool: 'bclaw_loop',
|
|
834
|
+
args: { intent: 'continue', loop_id: source.id, action_index: req.action_index },
|
|
835
|
+
when: 'retry after the current continuation owner settles',
|
|
836
|
+
}];
|
|
837
|
+
return handled;
|
|
838
|
+
}
|
|
839
|
+
const downstreamId = continuation.downstream?.id;
|
|
840
|
+
if (!downstreamId)
|
|
841
|
+
throw new Error('continuation_applied_without_downstream');
|
|
842
|
+
let loop = getLoop(downstreamId, options.cwd);
|
|
843
|
+
if (!loop)
|
|
844
|
+
throw new Error('continuation_downstream_disappeared');
|
|
845
|
+
let bind;
|
|
846
|
+
if (loop.kind === 'implementation') {
|
|
847
|
+
const bound = await handleBclawLoop({
|
|
848
|
+
args: { intent: 'bind', loop_id: downstreamId, agent: actor, agentId },
|
|
849
|
+
cwd: options.cwd,
|
|
850
|
+
defaultActor: actor,
|
|
851
|
+
sessionId: options.sessionId,
|
|
852
|
+
});
|
|
853
|
+
if (bound.response.status !== 'ok')
|
|
854
|
+
throw new Error(bound.response.error ?? bound.summary);
|
|
855
|
+
bind = bound.response.result;
|
|
856
|
+
loop = getLoop(downstreamId, options.cwd);
|
|
857
|
+
if (!loop)
|
|
858
|
+
throw new Error('continuation_downstream_disappeared');
|
|
859
|
+
}
|
|
860
|
+
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}`);
|
|
861
|
+
}
|
|
595
862
|
case 'bind': {
|
|
596
863
|
// Implementation bind is engine-only: validate the linked sequence and
|
|
597
864
|
// 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
|
};
|