brainclaw 1.26.2 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-coordination.js +65 -1
- package/dist/commands/attempt-authority.js +80 -0
- package/dist/commands/harvest.js +140 -61
- package/dist/commands/loop.js +34 -0
- package/dist/commands/loops-handlers.js +143 -15
- package/dist/commands/mcp-catalog.js +52 -18
- package/dist/commands/mcp-schemas.generated.js +64 -0
- package/dist/commands/mcp-write-claims.js +128 -1
- package/dist/commands/mcp-write-coordination.js +149 -76
- package/dist/core/agent-capability.js +1 -1
- package/dist/core/agentrun-reconciler.js +148 -22
- package/dist/core/agentruns.js +254 -29
- package/dist/core/assignment-request-schema.js +7 -0
- package/dist/core/assignment-sweeper.js +5 -3
- package/dist/core/assignments.js +131 -33
- package/dist/core/claim-request-schema.js +7 -0
- package/dist/core/claims.js +53 -2
- package/dist/core/dispatch-status.js +16 -6
- package/dist/core/dispatcher.js +51 -51
- package/dist/core/entity-operations.js +20 -0
- package/dist/core/events.js +4 -0
- package/dist/core/execution-adapters.js +189 -14
- package/dist/core/execution-contract.js +345 -0
- package/dist/core/execution.js +130 -16
- package/dist/core/facade-schema.js +3 -0
- package/dist/core/harness-adapters/base.js +150 -0
- package/dist/core/harness-adapters/claude.js +39 -0
- package/dist/core/harness-adapters/codex.js +57 -0
- package/dist/core/harness-adapters/harvest.js +109 -0
- package/dist/core/harness-adapters/index.js +8 -0
- package/dist/core/harness-adapters/prompt-only.js +13 -0
- package/dist/core/harness-adapters/registry.js +48 -0
- package/dist/core/harness-adapters/result.js +33 -0
- package/dist/core/harness-adapters/types.js +2 -0
- package/dist/core/ideation-loop-close.js +25 -2
- package/dist/core/instruction-templates.js +3 -2
- package/dist/core/loop-turn-dispatch.js +235 -0
- package/dist/core/loops/artifact-contract.js +11 -0
- package/dist/core/loops/attempt-authority.js +496 -0
- package/dist/core/loops/attempt-generations.js +509 -0
- package/dist/core/loops/attempt-reservation.js +197 -35
- package/dist/core/loops/attempt-rollout.js +404 -0
- package/dist/core/loops/attempt-takeover.js +155 -0
- package/dist/core/loops/bootstrap-acquire.js +7 -3
- package/dist/core/loops/brief-assembly.js +21 -4
- package/dist/core/loops/evidence.js +188 -0
- package/dist/core/loops/facade-schema.js +75 -11
- package/dist/core/loops/gate-policy.js +533 -0
- package/dist/core/loops/impl-bind.js +91 -81
- package/dist/core/loops/index.js +9 -0
- package/dist/core/loops/iteration-engine.js +31 -19
- package/dist/core/loops/kind-policies.js +90 -0
- package/dist/core/loops/lock.js +71 -13
- package/dist/core/loops/reconcile-turn.js +237 -18
- package/dist/core/loops/result-reducers.js +113 -10
- package/dist/core/loops/store.js +34 -3
- package/dist/core/loops/turn-execution.js +480 -0
- package/dist/core/loops/types.js +127 -3
- package/dist/core/loops/verbs.js +335 -99
- package/dist/core/loops/verify-command.js +105 -20
- package/dist/core/loops/workspace-digest.js +54 -0
- package/dist/core/review-loop-close.js +25 -3
- package/dist/core/review-loop-turn-dispatch.js +210 -161
- package/dist/core/runtime-signals.js +62 -25
- package/dist/core/schema.js +40 -0
- package/dist/core/spawn-check.js +3 -2
- package/dist/core/upgrades/backup.js +27 -4
- package/dist/facts.js +9 -8
- package/dist/facts.json +8 -7
- package/docs/cli.md +49 -1
- package/docs/concepts/attempt-authority.md +407 -0
- package/docs/concepts/evidence-attestations.md +135 -0
- package/docs/concepts/execution-contract.md +166 -0
- package/docs/concepts/harness-adapters.md +166 -0
- package/docs/concepts/ideation-loop.md +5 -4
- package/docs/concepts/loop-engine.md +302 -113
- package/docs/index.md +4 -1
- package/docs/integrations/codex.md +3 -3
- package/docs/integrations/mcp.md +59 -5
- package/docs/loops/debug.md +144 -0
- package/docs/loops/ideation.md +158 -0
- package/docs/loops/implementation.md +174 -0
- package/docs/loops/research.md +136 -0
- package/docs/loops/review.md +200 -0
- package/docs/mcp-schema-changelog.md +18 -5
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { ZodError } from 'zod';
|
|
2
2
|
import { listAgentRuns } from '../core/agentruns.js';
|
|
3
3
|
import { reconcileAgentRun } from '../core/agentrun-reconciler.js';
|
|
4
|
+
import { dispatchLoopTurn } from '../core/loop-turn-dispatch.js';
|
|
4
5
|
import { findReservationByRunId } from '../core/loops/attempt-reservation.js';
|
|
5
6
|
import { runVerify } from '../core/loops/verify-command.js';
|
|
6
7
|
import { runImplBind } from '../core/loops/impl-bind.js';
|
|
7
|
-
import { add_artifact, advance, AwaitingFileApplyApprovalError, closeLoop, complete_turn, computeNextExpected, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, provideInput, requestInput, resume, sweepPauseTimeouts, turn, VersionConflictError, withLoopLock, } from '../core/loops/index.js';
|
|
8
|
+
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, } from '../core/loops/index.js';
|
|
8
9
|
import { BclawLoopRequestSchema, BCLAW_LOOP_INTENTS, } from '../core/loops/facade-schema.js';
|
|
9
10
|
// NextExpectedHint type now lives in src/core/loops/next-expected.ts
|
|
10
11
|
// (hoisted per can_e57c7782 follow-up so MCP facade + CLI share the
|
|
@@ -15,6 +16,10 @@ function resolveActor(req, defaultActor) {
|
|
|
15
16
|
return { actor, agentId };
|
|
16
17
|
}
|
|
17
18
|
function successResponse(intent, result, artifacts, side_effects, warnings, durationMs, summary) {
|
|
19
|
+
const resultLoop = result && typeof result === 'object' && 'loop' in result
|
|
20
|
+
? result.loop
|
|
21
|
+
: undefined;
|
|
22
|
+
const nextActions = resultLoop ? pipelineNextActions(resultLoop) : [];
|
|
18
23
|
return {
|
|
19
24
|
response: {
|
|
20
25
|
status: 'ok',
|
|
@@ -24,10 +29,59 @@ function successResponse(intent, result, artifacts, side_effects, warnings, dura
|
|
|
24
29
|
side_effects,
|
|
25
30
|
warnings,
|
|
26
31
|
duration_ms: durationMs,
|
|
32
|
+
...(nextActions.length > 0 ? { next_actions: nextActions } : {}),
|
|
27
33
|
},
|
|
28
34
|
summary,
|
|
29
35
|
};
|
|
30
36
|
}
|
|
37
|
+
/** Cross-loop affordances: explicit next calls, never hidden orchestration. */
|
|
38
|
+
function pipelineNextActions(loop) {
|
|
39
|
+
if (loop.kind === 'ideation') {
|
|
40
|
+
const draft = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'plan_draft');
|
|
41
|
+
if (!draft || (loop.current_phase !== 'synthesis' && loop.status !== 'completed'))
|
|
42
|
+
return [];
|
|
43
|
+
const planIds = loop.linked?.plan_ids ?? [];
|
|
44
|
+
const sequenceIds = loop.linked?.sequence_ids ?? [];
|
|
45
|
+
if (planIds.length > 0 && sequenceIds.length > 0) {
|
|
46
|
+
return [{
|
|
47
|
+
tool: 'bclaw_loop',
|
|
48
|
+
args: {
|
|
49
|
+
intent: 'open', kind: 'implementation', title: `Implement ${loop.title}`,
|
|
50
|
+
goal: loop.goal, linked: { plan_ids: planIds, sequence_ids: sequenceIds, source_loop_id: loop.id },
|
|
51
|
+
verify: draft.implementation_verify,
|
|
52
|
+
slots: [{ role: 'implementer' }], allow_orphan: true,
|
|
53
|
+
},
|
|
54
|
+
when: 'start implementation from the accepted synthesis',
|
|
55
|
+
}];
|
|
56
|
+
}
|
|
57
|
+
return [{
|
|
58
|
+
tool: 'bclaw_create',
|
|
59
|
+
args: { entity: 'plan', text: draft.body ?? '<materialize the plan_draft artifact>', status: 'todo' },
|
|
60
|
+
when: 'materialize the synthesis before opening its implementation loop',
|
|
61
|
+
}];
|
|
62
|
+
}
|
|
63
|
+
if (loop.kind === 'implementation' && (loop.current_phase === 'handoff_ready' || loop.status === 'completed')) {
|
|
64
|
+
const handoff = [...loop.artifacts].reverse().find((artifact) => artifact.type === 'handoff');
|
|
65
|
+
const reviewScope = [...new Set(loop.slots.map((slot) => slot.scope_hint?.trim()).filter((scope) => Boolean(scope)))].join(',');
|
|
66
|
+
return [{
|
|
67
|
+
tool: 'bclaw_coordinate',
|
|
68
|
+
args: {
|
|
69
|
+
intent: 'review', open_loop: true,
|
|
70
|
+
task: handoff?.ref
|
|
71
|
+
? `Review implementation loop ${loop.id}; handoff ${handoff.ref.kind}:${handoff.ref.id}`
|
|
72
|
+
: `Review implementation loop ${loop.id} (${loop.title})`,
|
|
73
|
+
targetAgents: ['<reviewer>'],
|
|
74
|
+
...(reviewScope ? { scope: reviewScope } : {}),
|
|
75
|
+
...(handoff?.ref && (handoff.ref.kind === 'commit' || handoff.ref.kind === 'branch')
|
|
76
|
+
? { ref: handoff.ref.id }
|
|
77
|
+
: {}),
|
|
78
|
+
linked: { source_loop_id: loop.id, plan_ids: loop.linked?.plan_ids, sequence_ids: loop.linked?.sequence_ids },
|
|
79
|
+
},
|
|
80
|
+
when: 'implementation evidence is handoff-ready',
|
|
81
|
+
}];
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
31
85
|
function errorResponse(intent, code, message, durationMs, result = null) {
|
|
32
86
|
return {
|
|
33
87
|
response: {
|
|
@@ -115,12 +169,11 @@ const SLOT_BOUND_INTENTS = new Set(['complete_turn']);
|
|
|
115
169
|
* took over" window — the verb will not proceed if the lock's mutation_id
|
|
116
170
|
* changed between `acquireLock` and `work` dispatch. It does NOT cover mid-verb
|
|
117
171
|
* fs operations: the verbs themselves (`openLoop`, `advance`, …) perform their
|
|
118
|
-
* atomic-rename + JSONL append without consulting the fence.
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* committing write.
|
|
172
|
+
* atomic-rename + JSONL append without consulting the fence. Safety therefore
|
|
173
|
+
* depends on lock.ts refusing deadline/lease-based takeover of a still-live
|
|
174
|
+
* local process (and failing closed for remote-host owners). If a future slice
|
|
175
|
+
* adds async work inside a mutation or enables time-based/remote takeover,
|
|
176
|
+
* `fenceCheck` must first be threaded to every committing write.
|
|
124
177
|
*/
|
|
125
178
|
function withLockedLoopMutation(req, agentId, cwd, work) {
|
|
126
179
|
return withLoopLock({
|
|
@@ -128,7 +181,7 @@ function withLockedLoopMutation(req, agentId, cwd, work) {
|
|
|
128
181
|
intent: req.intent,
|
|
129
182
|
agentId,
|
|
130
183
|
scope: { kind: 'loop', loopId: req.loop_id },
|
|
131
|
-
expectedVersion: req.expected_version,
|
|
184
|
+
expectedVersion: 'expected_version' in req ? req.expected_version : undefined,
|
|
132
185
|
clientRequestId: req.client_request_id,
|
|
133
186
|
requestPayload: requestPayload(req),
|
|
134
187
|
currentVersion: () => currentLoopVersion(req.loop_id, cwd),
|
|
@@ -295,6 +348,45 @@ export async function handleBclawLoop(options) {
|
|
|
295
348
|
return successResponse('list', { loops: sliced, total: loops.length }, sliced.map((l) => loopArtifactEntry(l.id)), [], [], Date.now() - startMs, `✔ list ${sliced.length}/${loops.length} loops`);
|
|
296
349
|
}
|
|
297
350
|
case 'turn': {
|
|
351
|
+
if (req.dispatch) {
|
|
352
|
+
if (!req.slot_id) {
|
|
353
|
+
return errorResponse('turn', 'validation_error', 'turn dispatch requires slot_id', Date.now() - startMs);
|
|
354
|
+
}
|
|
355
|
+
const dispatched = await dispatchLoopTurn({
|
|
356
|
+
loop_id: req.loop_id,
|
|
357
|
+
slot_id: req.slot_id,
|
|
358
|
+
task: req.input ?? `Execute ${req.loop_id} slot ${req.slot_id}`,
|
|
359
|
+
dispatcher_agent: actor,
|
|
360
|
+
dispatcher_agent_id: req.agentId,
|
|
361
|
+
session_id: options.sessionId,
|
|
362
|
+
model: req.model,
|
|
363
|
+
auto_execute: req.auto_execute,
|
|
364
|
+
candidate_agents: req.target_agents,
|
|
365
|
+
cwd: options.cwd ?? process.cwd(),
|
|
366
|
+
});
|
|
367
|
+
// Before AttemptAuthority exists an error is a true denial. Once the
|
|
368
|
+
// launch grant has crossed, however, transport may fall back to a
|
|
369
|
+
// manual command or become crossed_unknown. Preserve the created
|
|
370
|
+
// entities in a successful structured response instead of reporting
|
|
371
|
+
// an empty-side-effect error that invites a dangerous retry.
|
|
372
|
+
if (dispatched.error && !dispatched.turn_id) {
|
|
373
|
+
return errorResponse('turn', 'dispatch_denied', dispatched.error, Date.now() - startMs);
|
|
374
|
+
}
|
|
375
|
+
const loop = getLoop(req.loop_id, options.cwd);
|
|
376
|
+
return successResponse('turn', { loop, dispatch: dispatched, next_expected: loop ? computeNextExpected(loop) : undefined }, [
|
|
377
|
+
loopArtifactEntry(req.loop_id),
|
|
378
|
+
...(dispatched.assignment_id ? [{ type: 'assignment', id: dispatched.assignment_id }] : []),
|
|
379
|
+
...(dispatched.run_id ? [{ type: 'agent_run', id: dispatched.run_id }] : []),
|
|
380
|
+
...(dispatched.claim_id ? [{ type: 'claim', id: dispatched.claim_id }] : []),
|
|
381
|
+
], [
|
|
382
|
+
sideEffectUpdate('loop', req.loop_id),
|
|
383
|
+
...(dispatched.claim_id ? [{ action: 'create', entity: 'claim', id: dispatched.claim_id }] : []),
|
|
384
|
+
...(dispatched.assignment_id ? [{ action: 'create', entity: 'assignment', id: dispatched.assignment_id }] : []),
|
|
385
|
+
...(dispatched.run_id ? [{ action: 'create', entity: 'agent_run', id: dispatched.run_id }] : []),
|
|
386
|
+
], dispatched.error ? [dispatched.error] : [], Date.now() - startMs, dispatched.error
|
|
387
|
+
? `⚠ ${dispatched.kind}.${dispatched.phase} turn ${dispatched.turn_id} crossed; ${dispatched.error}`
|
|
388
|
+
: `✔ dispatched ${dispatched.kind}.${dispatched.phase} turn ${dispatched.turn_id}`);
|
|
389
|
+
}
|
|
298
390
|
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
299
391
|
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
300
392
|
const loop = turn({
|
|
@@ -316,6 +408,13 @@ export async function handleBclawLoop(options) {
|
|
|
316
408
|
const loop = complete_turn({
|
|
317
409
|
id: req.loop_id,
|
|
318
410
|
slot_id: req.slot_id,
|
|
411
|
+
assignment_id: req.assignment_id,
|
|
412
|
+
turn_id: req.turn_id,
|
|
413
|
+
run_id: req.run_id,
|
|
414
|
+
nonce: req.nonce,
|
|
415
|
+
attempt_epoch: req.attempt_epoch,
|
|
416
|
+
execution_contract_hash: req.execution_contract_hash,
|
|
417
|
+
workspace_digest: req.workspace_digest,
|
|
319
418
|
outcome: req.outcome,
|
|
320
419
|
failure_reason: req.failure_reason,
|
|
321
420
|
artifact: req.artifact
|
|
@@ -325,6 +424,7 @@ export async function handleBclawLoop(options) {
|
|
|
325
424
|
body: req.artifact.body,
|
|
326
425
|
ref: req.artifact.ref,
|
|
327
426
|
addresses_critique: req.artifact.addresses_critique,
|
|
427
|
+
implementation_verify: req.artifact.implementation_verify,
|
|
328
428
|
}
|
|
329
429
|
: undefined,
|
|
330
430
|
actor,
|
|
@@ -337,6 +437,32 @@ export async function handleBclawLoop(options) {
|
|
|
337
437
|
return successResponse('complete_turn', { loop, next_expected: computeNextExpected(loop) }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
338
438
|
});
|
|
339
439
|
}
|
|
440
|
+
case 'takeover': {
|
|
441
|
+
const authorityHome = readLocalAuthorityHome(options.cwd ?? process.cwd());
|
|
442
|
+
if (!authorityHome) {
|
|
443
|
+
return errorResponse('takeover', 'authority_home_unavailable', 'local store/device authority identity is not initialized', Date.now() - startMs);
|
|
444
|
+
}
|
|
445
|
+
const result = takeoverLoopAttempt({
|
|
446
|
+
loop_id: req.loop_id,
|
|
447
|
+
slot_id: req.slot_id,
|
|
448
|
+
turn_id: req.turn_id,
|
|
449
|
+
expected_epoch: req.expected_epoch,
|
|
450
|
+
authority_home: authorityHome,
|
|
451
|
+
actor,
|
|
452
|
+
actor_id: agentId,
|
|
453
|
+
writer_id: agentId,
|
|
454
|
+
cause: req.cause,
|
|
455
|
+
liveness_evidence: req.liveness_evidence,
|
|
456
|
+
external_effect_policy: req.external_effect_policy,
|
|
457
|
+
next_workspace_path: req.next_workspace_path,
|
|
458
|
+
mode: req.takeover_mode,
|
|
459
|
+
cwd: options.cwd ?? process.cwd(),
|
|
460
|
+
});
|
|
461
|
+
return successResponse('takeover', {
|
|
462
|
+
...result,
|
|
463
|
+
next_action: 'dispatch the same logical turn; the common path will project and contend on launch(next_epoch)',
|
|
464
|
+
}, [loopArtifactEntry(result.loop.id)], [sideEffectUpdate('loop', result.loop.id)], [], Date.now() - startMs, `✔ takeover ${result.turn_id} epoch=${result.attempt_epoch} run=${result.run_id} (armed, not spawned)`);
|
|
465
|
+
}
|
|
340
466
|
case 'advance': {
|
|
341
467
|
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
342
468
|
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
@@ -360,9 +486,9 @@ export async function handleBclawLoop(options) {
|
|
|
360
486
|
phase: req.artifact.phase,
|
|
361
487
|
type: req.artifact.type,
|
|
362
488
|
body: req.artifact.body,
|
|
363
|
-
produced_by: req.artifact.produced_by,
|
|
364
489
|
ref: req.artifact.ref,
|
|
365
490
|
addresses_critique: req.artifact.addresses_critique,
|
|
491
|
+
implementation_verify: req.artifact.implementation_verify,
|
|
366
492
|
},
|
|
367
493
|
actor,
|
|
368
494
|
}, options.cwd);
|
|
@@ -451,7 +577,7 @@ export async function handleBclawLoop(options) {
|
|
|
451
577
|
return errorResponse('verify', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
452
578
|
}
|
|
453
579
|
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
454
|
-
const result = runVerify({ loop_id: req.loop_id, actor }, options.cwd);
|
|
580
|
+
const result = runVerify({ loop_id: req.loop_id, slot_id: req.slot_id, actor }, options.cwd);
|
|
455
581
|
const newEvents = findNewLoopEvents(result.thread.id, beforeEvents, options.cwd);
|
|
456
582
|
const summary = result.unconfigured
|
|
457
583
|
? `verify: loop has no protocol.verify — falling back to an agent-narrated verify_report`
|
|
@@ -467,10 +593,9 @@ export async function handleBclawLoop(options) {
|
|
|
467
593
|
}, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summary);
|
|
468
594
|
}
|
|
469
595
|
case 'bind': {
|
|
470
|
-
//
|
|
471
|
-
// bind
|
|
472
|
-
//
|
|
473
|
-
// coordinate(open_loop)'s async-handler-spawns pattern, not a synchronous verb.
|
|
596
|
+
// Implementation bind is engine-only: validate the linked sequence and
|
|
597
|
+
// advance bind -> execute. Worker launch belongs exclusively to
|
|
598
|
+
// turn(dispatch=true), the common AttemptAuthority path.
|
|
474
599
|
const existing = getLoop(req.loop_id, options.cwd);
|
|
475
600
|
if (!existing) {
|
|
476
601
|
return errorResponse('bind', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
@@ -504,7 +629,7 @@ export async function handleBclawLoop(options) {
|
|
|
504
629
|
dispatched: bind.messages_sent,
|
|
505
630
|
dispatch: bind.dispatch,
|
|
506
631
|
next_expected: computeNextExpected(loop),
|
|
507
|
-
}, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], sideEffects, bind.dispatch?.warnings ?? [], Date.now() - startMs, bind.reason);
|
|
632
|
+
}, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], sideEffects, [...bind.warnings, ...(bind.dispatch?.warnings ?? [])], Date.now() - startMs, bind.reason);
|
|
508
633
|
}
|
|
509
634
|
}
|
|
510
635
|
}
|
|
@@ -538,6 +663,9 @@ export async function handleBclawLoop(options) {
|
|
|
538
663
|
});
|
|
539
664
|
}
|
|
540
665
|
const message = err instanceof Error ? err.message : String(err);
|
|
666
|
+
if (message.startsWith('attempt_fence_')) {
|
|
667
|
+
return errorResponse(req.intent, 'attempt_fence_rejected', message, Date.now() - startMs);
|
|
668
|
+
}
|
|
541
669
|
if (message.includes('unauthorized_slot_write')) {
|
|
542
670
|
return errorResponse(req.intent, 'unauthorized_slot_write', message, Date.now() - startMs);
|
|
543
671
|
}
|
|
@@ -854,6 +854,16 @@ const MCP_WRITE_TOOLS = [
|
|
|
854
854
|
targetAgents: { type: 'array', items: { type: 'string' }, description: 'Agent names to target. If omitted, all spawnable agents are used.' },
|
|
855
855
|
constraints: { type: 'object', description: 'Optional structured constraints passed alongside the brief (e.g. deadline, reviewCriteria).' },
|
|
856
856
|
threadId: { type: 'string', description: 'Thread ID for summarize intent.' },
|
|
857
|
+
linked: {
|
|
858
|
+
type: 'object',
|
|
859
|
+
description: 'Optional pipeline provenance persisted on a review loop opened by this call.',
|
|
860
|
+
properties: {
|
|
861
|
+
plan_ids: { type: 'array', items: { type: 'string' } },
|
|
862
|
+
sequence_ids: { type: 'array', items: { type: 'string' } },
|
|
863
|
+
source_loop_id: { type: 'string', pattern: '^lop_[0-9a-z]+$' },
|
|
864
|
+
},
|
|
865
|
+
additionalProperties: false,
|
|
866
|
+
},
|
|
857
867
|
autoExecute: { type: 'boolean', description: 'Attempt to spawn target agents after delivery (default: true). Applies to the spawning intents assign/review/reroute AND to multi-agent ideate (with targetAgents, it spawns one worktree-isolated critic worker per target). consult is inbox-only and ignores autoExecute; summarize just reads a thread and ignores it. When false on a spawning intent, returns command_ready_manual with bash commands for the supervisor to run.' },
|
|
858
868
|
open_loop: { type: 'boolean', description: 'For intent=review only: also open a review Loop on top of the candidate (author + reviewer slots, advance to `findings`, dispatch turns). Default false — existing review callers are unaffected. See docs/concepts/loop-engine.md §Automation.' },
|
|
859
869
|
review_mode: { type: 'string', enum: ['asymmetric', 'symmetric'], description: 'Optional review Loop mode when open_loop=true. `asymmetric` (default) keeps the classical author→reviewer handoff; `symmetric` lets each reviewer turn also apply fixes directly, halving round-trips for spec/doc reviews. Ignored when open_loop is false.' },
|
|
@@ -872,7 +882,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
872
882
|
},
|
|
873
883
|
{
|
|
874
884
|
name: 'bclaw_loop',
|
|
875
|
-
description: 'Loop engine facade: open/turn/complete_turn/advance/add_artifact/pause/resume/close/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/request_input/provide_input/get/list multi-turn work loops (review, ideation, implementation, research, debug). Direct open requires allow_orphan=true because the caller owns subsequent dispatch. `takeover` fences one physical run and arms a fresh generation; it never changes protocol gates.',
|
|
876
886
|
// schemaSource is informational for now — grep target so future migrators
|
|
877
887
|
// can locate zod-derived tools quickly. The parity test in
|
|
878
888
|
// tests/unit/mcp-zod-parity.test.ts hard-codes its (tool, zod-schema)
|
|
@@ -890,12 +900,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
890
900
|
properties: {
|
|
891
901
|
intent: {
|
|
892
902
|
type: 'string',
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
// nothing ever ran. Loops are opened via
|
|
896
|
-
// bclaw_coordinate(intent='review', open_loop=true) or intent='ideate'.
|
|
897
|
-
enum: ['get', 'list', 'turn', 'complete_turn', 'advance', 'add_artifact', 'pause', 'resume', 'close', 'bind'],
|
|
898
|
-
description: 'Loop lifecycle intent for driving turns inside a loop that was already opened via the coordinate facade. To START a loop, use `bclaw_coordinate(intent="review", open_loop=true, targetAgents=[…])` or `intent="ideate"` — that opens the loop AND dispatches the first turn. `bind` (implementation loops only) dispatches the loop\'s linked sequence and advances bind→execute — the engine action for the `bind` phase. See docs/concepts/loop-engine.md.',
|
|
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. `verify` runs the configured verification; request_input/provide_input are cross-kind clarification primitives.',
|
|
899
905
|
},
|
|
900
906
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
901
907
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -906,28 +912,56 @@ const MCP_WRITE_TOOLS = [
|
|
|
906
912
|
linked: { type: 'object', description: 'Optional top-level plan/sequence refs (open).' },
|
|
907
913
|
stop_condition: { type: 'object', description: 'Optional stop_condition override (open). Composite any/all supported.' },
|
|
908
914
|
mode: { type: 'string', enum: ['asymmetric', 'symmetric'], description: 'Review mode selector for open (review kind only).' },
|
|
915
|
+
allow_orphan: { type: 'boolean', description: 'Required true for direct open: acknowledges that the caller owns subsequent turn/bind/dispatch.' },
|
|
916
|
+
verify: { type: 'object', description: 'Optional open-time verification policy, e.g. { command: ["npm", "test"] }.' },
|
|
909
917
|
status: { type: 'string', description: 'For intent="list": filter value (any loop status). For intent="close": target final status — accepted values are `completed` | `cancelled` | `blocked` only (NOT `failed`; map crashed/dead loops to `cancelled` with a `reason`).' },
|
|
910
918
|
include_events: { type: 'boolean', description: 'get: include the event journal in the response.' },
|
|
911
919
|
limit: { type: 'number', description: 'list: max loops returned.' },
|
|
912
920
|
offset: { type: 'number', description: 'list: pagination offset.' },
|
|
913
|
-
slot_id: { type: 'string', description: 'Slot id for turn / complete_turn.' },
|
|
921
|
+
slot_id: { type: 'string', description: 'Slot id for turn / complete_turn / takeover.' },
|
|
922
|
+
turn_id: { type: 'string', description: 'takeover: stable logical turn id; complete_turn: required as part of the full AttemptAuthority v2 generation fence.' },
|
|
923
|
+
expected_epoch: { type: 'number', description: 'takeover: active physical generation epoch expected by the caller.' },
|
|
924
|
+
cause: { type: 'string', description: 'takeover: audited reason for fencing the current producer.' },
|
|
925
|
+
liveness_evidence: { type: 'string', description: 'takeover: concrete evidence that the current producer cannot safely continue.' },
|
|
926
|
+
external_effect_policy: { type: 'string', enum: ['none', 'idempotent', 'externally_fenced'], description: 'takeover: declaration required before automatic re-execution.' },
|
|
927
|
+
next_workspace_path: { type: 'string', description: 'takeover: existing isolated workspace for the successor generation.' },
|
|
928
|
+
takeover_mode: { type: 'string', enum: ['takeover', 'retry'], description: 'takeover: causal close decision kind (default takeover).' },
|
|
914
929
|
role: { type: 'string', description: 'Slot role for turn (resolves the first non-done slot with that role).' },
|
|
915
930
|
input: { type: 'string', description: 'turn: free-form input passed to the slot.' },
|
|
916
|
-
assignment_id: { type: 'string', description: 'turn: assignment id produced by
|
|
917
|
-
|
|
931
|
+
assignment_id: { type: 'string', description: 'turn: assignment id produced by an external dispatcher; complete_turn: current logical assignment id in the full v2 fence.' },
|
|
932
|
+
run_id: { type: 'string', description: 'complete_turn: current physical AgentRun id in the full AttemptAuthority v2 fence.' },
|
|
933
|
+
nonce: { type: 'string', description: 'complete_turn: current immutable launch nonce in the full AttemptAuthority v2 fence.' },
|
|
934
|
+
attempt_epoch: { type: 'number', description: 'complete_turn: current physical generation epoch in the full AttemptAuthority v2 fence.' },
|
|
935
|
+
execution_contract_hash: { type: 'string', description: 'complete_turn: SHA-256 hash of the current generation ExecutionContract.' },
|
|
936
|
+
workspace_digest: { type: 'string', description: 'complete_turn: digest binding the current generation to its isolated workspace.' },
|
|
937
|
+
dispatch: { type: 'boolean', description: 'turn: when true, run the trusted production driver (claim + AttemptAuthority + inbox + real worker launch). Plain turn remains a Loop Engine state mutation and never spawns.' },
|
|
918
938
|
outcome: { type: 'string', enum: ['done', 'failed', 'cancelled'], description: 'complete_turn outcome (default done).' },
|
|
919
939
|
failure_reason: { type: 'string', description: 'complete_turn: optional failure/cancel reason.' },
|
|
920
940
|
artifact: { type: 'object', description: 'complete_turn / add_artifact payload: { phase, type, body?, produced_by?, ref? }.' },
|
|
921
|
-
dry_run: { type: 'boolean', description: 'bind:
|
|
922
|
-
lanes: { type: 'array', items: { type: 'string' }, description: 'bind
|
|
923
|
-
auto_execute: { type: 'boolean', description: '
|
|
924
|
-
model: { type: 'string', description: '
|
|
925
|
-
|
|
941
|
+
dry_run: { type: 'boolean', description: 'bind: validate the linked sequence without advancing; bind never spawns.' },
|
|
942
|
+
lanes: { type: 'array', items: { type: 'string' }, description: 'Deprecated bind launch option retained for compatibility and ignored; dispatch independent slots with turn(dispatch=true).' },
|
|
943
|
+
auto_execute: { type: 'boolean', description: 'turn dispatch: false returns a contract-wrapped manual launch command. Deprecated and ignored for engine-only bind.' },
|
|
944
|
+
model: { type: 'string', description: 'turn dispatch: model override. Deprecated and ignored for engine-only bind.' },
|
|
945
|
+
target_agents: { type: 'array', items: { type: 'string' }, description: 'turn dispatch: deterministic capability candidate pool used when the slot has no frozen agent.' },
|
|
946
|
+
max_assignments: { type: 'number', description: 'Deprecated bind launch option retained for compatibility and ignored.' },
|
|
926
947
|
to_phase: { type: 'string', description: 'advance: explicit target phase (otherwise the next phase).' },
|
|
927
948
|
force: { type: 'boolean', description: 'advance: allow going backwards (increments iteration_count).' },
|
|
928
949
|
reason: { type: 'string', description: 'advance / pause / close: optional reason string.' },
|
|
929
|
-
expected_version: { type: 'number', description: '
|
|
930
|
-
|
|
950
|
+
expected_version: { type: 'number', description: 'Optimistic-CAS version enforced under the loop lock for mutating intents; stale values fail with version_conflict before mutation.' },
|
|
951
|
+
phase: { type: 'string', description: 'request_input: current loop phase.' },
|
|
952
|
+
question_text: { type: 'string', description: 'request_input: operator question (max 500 characters).' },
|
|
953
|
+
evidence: { type: 'array', items: { type: 'string' }, description: 'request_input: concrete evidence motivating the question.' },
|
|
954
|
+
suggested_default: { type: 'string', description: 'request_input: optional default answer.' },
|
|
955
|
+
options: { type: 'array', items: { type: 'object' }, description: 'request_input: optional 2-4 structured choices.' },
|
|
956
|
+
pause_scope: { type: 'string', enum: ['slot', 'loop'], description: 'request_input: pause only the slot or the whole loop.' },
|
|
957
|
+
on_timeout: { type: 'string', enum: ['use_default', 'cancel_loop', 'continue_incomplete'], description: 'request_input: timeout policy.' },
|
|
958
|
+
timeout_at: { type: 'string', description: 'request_input: optional ISO timestamp.' },
|
|
959
|
+
replies_to: { type: 'string', description: 'provide_input: question id (qst_…).' },
|
|
960
|
+
resolved_via: { type: 'string', enum: ['answer', 'choose', 'skip', 'timeout_default'], description: 'provide_input: how the answer was resolved.' },
|
|
961
|
+
answer_text: { type: 'string', description: 'provide_input: free-form answer.' },
|
|
962
|
+
chosen_option_id: { type: 'string', description: 'provide_input: selected option id.' },
|
|
963
|
+
by: { type: 'string', enum: ['operator', 'system'], description: 'provide_input: answer actor (default operator).' },
|
|
964
|
+
client_request_id: { type: 'string', description: 'Idempotency key enforced for mutating intents: exact replay returns the cached result; owner/payload reuse conflicts fail closed.' },
|
|
931
965
|
project: { type: 'string', description: 'Optional linked project name/path. Routes loop reads and mutations to that project. Defaults to the current cwd.' },
|
|
932
966
|
agent: { type: 'string', description: 'Caller agent name.' },
|
|
933
967
|
agentId: { type: 'string', description: 'Caller registered agent id (enforced for slot-bound auth in complete_turn).' },
|
|
@@ -937,7 +971,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
937
971
|
},
|
|
938
972
|
{
|
|
939
973
|
name: 'bclaw_assignment_update',
|
|
940
|
-
description: 'Report assignment lifecycle status. Part of the Agent SDK runtime protocol.
|
|
974
|
+
description: 'Report assignment lifecycle status. Part of the Agent SDK runtime protocol. Legacy workers may report accepted/started/progress/completed/failed/blocked. For an AttemptAuthority v2 logical Assignment, the complete current generation fence is mandatory and only accepted/started/progress are allowed; terminal outcome goes through full-fence LANE-RESULT settlement, which then projects Assignment/Claim convergence. The assignment_id is provided in the dispatch brief. OWNERSHIP (trp#291): only the assigned agent may update it. Coordinators should verify via bclaw_dispatch_status rather than impersonating the worker.',
|
|
941
975
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
942
976
|
inputSchema: { ...generatedSchemas.AssignmentUpdateRequest },
|
|
943
977
|
},
|
|
@@ -283,6 +283,26 @@ export const generatedSchemas = {
|
|
|
283
283
|
"phase": {
|
|
284
284
|
"type": "string"
|
|
285
285
|
},
|
|
286
|
+
"lane": {
|
|
287
|
+
"type": "string"
|
|
288
|
+
},
|
|
289
|
+
"scope_hint": {
|
|
290
|
+
"type": "string"
|
|
291
|
+
},
|
|
292
|
+
"plan_ids": {
|
|
293
|
+
"type": "array",
|
|
294
|
+
"items": {
|
|
295
|
+
"type": "string",
|
|
296
|
+
"minLength": 1
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
"step_ids": {
|
|
300
|
+
"type": "array",
|
|
301
|
+
"items": {
|
|
302
|
+
"type": "string",
|
|
303
|
+
"minLength": 1
|
|
304
|
+
}
|
|
305
|
+
},
|
|
286
306
|
"status": {
|
|
287
307
|
"type": "string",
|
|
288
308
|
"enum": [
|
|
@@ -650,6 +670,28 @@ export const generatedSchemas = {
|
|
|
650
670
|
"coordinator_override": {
|
|
651
671
|
"type": "boolean",
|
|
652
672
|
"description": "Opt-in override for a trusted+ caller releasing a claim they do NOT own (cross-agent teardown, ghost-claim cleanup). Rejected for contributor-level callers; audited when used. trp#928."
|
|
673
|
+
},
|
|
674
|
+
"turn_id": {
|
|
675
|
+
"type": "string"
|
|
676
|
+
},
|
|
677
|
+
"run_id": {
|
|
678
|
+
"type": "string"
|
|
679
|
+
},
|
|
680
|
+
"nonce": {
|
|
681
|
+
"type": "string"
|
|
682
|
+
},
|
|
683
|
+
"attempt_epoch": {
|
|
684
|
+
"type": "integer",
|
|
685
|
+
"minimum": 0,
|
|
686
|
+
"maximum": 9007199254740991
|
|
687
|
+
},
|
|
688
|
+
"execution_contract_hash": {
|
|
689
|
+
"type": "string",
|
|
690
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
691
|
+
},
|
|
692
|
+
"workspace_digest": {
|
|
693
|
+
"type": "string",
|
|
694
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
653
695
|
}
|
|
654
696
|
},
|
|
655
697
|
"required": [
|
|
@@ -1014,6 +1056,28 @@ export const generatedSchemas = {
|
|
|
1014
1056
|
],
|
|
1015
1057
|
"description": "Optional ActionRequired payload when status=blocked. Lets the worker request approval, user input, or clarification before resuming."
|
|
1016
1058
|
},
|
|
1059
|
+
"turn_id": {
|
|
1060
|
+
"type": "string"
|
|
1061
|
+
},
|
|
1062
|
+
"run_id": {
|
|
1063
|
+
"type": "string"
|
|
1064
|
+
},
|
|
1065
|
+
"nonce": {
|
|
1066
|
+
"type": "string"
|
|
1067
|
+
},
|
|
1068
|
+
"attempt_epoch": {
|
|
1069
|
+
"type": "integer",
|
|
1070
|
+
"minimum": 0,
|
|
1071
|
+
"maximum": 9007199254740991
|
|
1072
|
+
},
|
|
1073
|
+
"execution_contract_hash": {
|
|
1074
|
+
"type": "string",
|
|
1075
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
1076
|
+
},
|
|
1077
|
+
"workspace_digest": {
|
|
1078
|
+
"type": "string",
|
|
1079
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
1080
|
+
},
|
|
1017
1081
|
"agent": {
|
|
1018
1082
|
"type": "string",
|
|
1019
1083
|
"description": "Agent name."
|
|
@@ -247,8 +247,9 @@ export async function handleBclawReleaseClaim(payload, ctx) {
|
|
|
247
247
|
};
|
|
248
248
|
}
|
|
249
249
|
const cwd = located.location?.cwd ?? payload.cwd;
|
|
250
|
+
let routedClaim;
|
|
250
251
|
try {
|
|
251
|
-
loadClaim(claimId, cwd); // validate existence before delegating
|
|
252
|
+
routedClaim = loadClaim(claimId, cwd); // validate existence before delegating
|
|
252
253
|
}
|
|
253
254
|
catch {
|
|
254
255
|
const scope = located.enumeration_incomplete
|
|
@@ -290,6 +291,30 @@ export async function handleBclawReleaseClaim(payload, ctx) {
|
|
|
290
291
|
};
|
|
291
292
|
}
|
|
292
293
|
}
|
|
294
|
+
if (routedClaim.assignment_id) {
|
|
295
|
+
const { findReservationByAssignmentId } = await import('../core/loops/attempt-reservation.js');
|
|
296
|
+
const { resolveTurnGenerationChain } = await import('../core/loops/attempt-generations.js');
|
|
297
|
+
const reservation = findReservationByAssignmentId(routedClaim.assignment_id, cwd);
|
|
298
|
+
const chain = reservation
|
|
299
|
+
? resolveTurnGenerationChain(reservation.store_root, reservation.turn_id)
|
|
300
|
+
: undefined;
|
|
301
|
+
if (reservation && chain) {
|
|
302
|
+
if (coordinatorOverrideRequested) {
|
|
303
|
+
const { getLoop } = await import('../core/loops/store.js');
|
|
304
|
+
const loop = getLoop(reservation.loop_id, cwd);
|
|
305
|
+
if (!loop || loop.created_by !== releaseIdentity.identity.agent_id) {
|
|
306
|
+
return {
|
|
307
|
+
response: createToolErrorResponse('trust_error', `AttemptAuthority v2 coordinator override requires the authenticated loop creator (${loop?.created_by ?? 'unknown'}); caller is ${releaseIdentity.identity.agent_id}`, { claim_id: claimId, loop_id: reservation.loop_id }),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
return {
|
|
313
|
+
response: createToolErrorResponse('validation_error', `Claim ${claimId} belongs to a logical AttemptAuthority v2 Assignment and is released only after immutable settlement; worker release is refused`, { claim_id: claimId, active_epoch: chain.latest_generation.attempt_epoch }),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
293
318
|
const releaseAuth = {
|
|
294
319
|
agent: releaseIdentity.identity.agent_name,
|
|
295
320
|
agent_id: releaseIdentity.identity.agent_id,
|
|
@@ -661,6 +686,108 @@ export async function handleBclawAssignmentUpdate(payload, ctx) {
|
|
|
661
686
|
if (assignment.agent !== callerAgent) {
|
|
662
687
|
return { response: createToolErrorResponse('trust_error', `Agent ${callerAgent} cannot update assignment owned by ${assignment.agent}`) };
|
|
663
688
|
}
|
|
689
|
+
// AttemptAuthority v2 keeps Assignment stable across physical generations.
|
|
690
|
+
// Therefore agent ownership alone is insufficient: a late epoch-0 worker
|
|
691
|
+
// still owns the same Assignment. Require its complete current fence before
|
|
692
|
+
// ANY progress/status mutation, AgentRun synchronization, or claim cascade.
|
|
693
|
+
const { findReservationByAssignmentId, evidenceMatchesAttempt } = await import('../core/loops/attempt-reservation.js');
|
|
694
|
+
const { resolveTurnGenerationChain } = await import('../core/loops/attempt-generations.js');
|
|
695
|
+
const reservation = findReservationByAssignmentId(assignmentId, cwd);
|
|
696
|
+
const generationChain = reservation
|
|
697
|
+
? resolveTurnGenerationChain(reservation.store_root, reservation.turn_id)
|
|
698
|
+
: undefined;
|
|
699
|
+
if (reservation && generationChain) {
|
|
700
|
+
const matches = evidenceMatchesAttempt(reservation, {
|
|
701
|
+
assignment_id: assignmentId,
|
|
702
|
+
turn_id: typeof args.turn_id === 'string' ? args.turn_id : undefined,
|
|
703
|
+
run_id: typeof args.run_id === 'string' ? args.run_id : undefined,
|
|
704
|
+
nonce: typeof args.nonce === 'string' ? args.nonce : undefined,
|
|
705
|
+
attempt_epoch: typeof args.attempt_epoch === 'number' ? args.attempt_epoch : undefined,
|
|
706
|
+
contract_hash: typeof args.execution_contract_hash === 'string' ? args.execution_contract_hash : undefined,
|
|
707
|
+
workspace_digest: typeof args.workspace_digest === 'string' ? args.workspace_digest : undefined,
|
|
708
|
+
});
|
|
709
|
+
if (!matches) {
|
|
710
|
+
return {
|
|
711
|
+
response: createToolErrorResponse('validation_error', `Assignment ${assignmentId} mutation rejected: missing or stale AttemptAuthority v2 generation fence`, { assignment_id: assignmentId, active_epoch: generationChain.latest_generation.attempt_epoch }),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// A v2 Assignment is the stable LOGICAL identity shared by every physical
|
|
716
|
+
// generation. Worker lifecycle reports therefore drive only the current
|
|
717
|
+
// AgentRun; terminal Assignment/Claim convergence is reserved for
|
|
718
|
+
// reconcileTurn after close(epoch)=settled. This also lets a successor
|
|
719
|
+
// acknowledge/start while the stable Assignment is already `started`.
|
|
720
|
+
if (generationChain) {
|
|
721
|
+
const generation = generationChain.latest_generation;
|
|
722
|
+
if (!['accepted', 'started', 'progress'].includes(status)) {
|
|
723
|
+
return {
|
|
724
|
+
response: createToolErrorResponse('validation_error', `AttemptAuthority v2 Assignment ${assignmentId} is logical and cannot transition to '${status}' from a worker; submit full-fence LANE-RESULT evidence for settlement`, { assignment_id: assignmentId, run_id: generation.run_id, attempt_epoch: generation.attempt_epoch }),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
if (status === 'accepted') {
|
|
728
|
+
if (assignment.status === 'offered') {
|
|
729
|
+
transitionAsgn(assignmentId, 'accepted', {
|
|
730
|
+
session_id: effectiveSessionId,
|
|
731
|
+
actor: callerAgent,
|
|
732
|
+
actor_id: resolved.identity.agent_id,
|
|
733
|
+
}, cwd);
|
|
734
|
+
}
|
|
735
|
+
else if (assignment.status !== 'accepted' && assignment.status !== 'started') {
|
|
736
|
+
return { response: createToolErrorResponse('operation_error', `Logical Assignment ${assignmentId} is ${assignment.status}, expected offered/accepted/started`) };
|
|
737
|
+
}
|
|
738
|
+
if (assignment.message_id) {
|
|
739
|
+
try {
|
|
740
|
+
const { ackMessage } = await import('../core/messaging.js');
|
|
741
|
+
ackMessage(assignment.message_id, callerAgent, cwd, { claimId: assignment.claim_id });
|
|
742
|
+
}
|
|
743
|
+
catch { /* best-effort */ }
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
response: toolResponse({
|
|
747
|
+
content: [{ type: 'text', text: `Attempt generation ${generation.attempt_epoch} accepted for logical Assignment ${assignmentId}` }],
|
|
748
|
+
assignment_id: assignmentId,
|
|
749
|
+
status: assignment.status === 'offered' ? 'accepted' : assignment.status,
|
|
750
|
+
run_id: generation.run_id,
|
|
751
|
+
attempt_epoch: generation.attempt_epoch,
|
|
752
|
+
}),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
if (status === 'started') {
|
|
756
|
+
if (assignment.status === 'accepted') {
|
|
757
|
+
transitionAsgn(assignmentId, 'started', {
|
|
758
|
+
session_id: effectiveSessionId,
|
|
759
|
+
actor: callerAgent,
|
|
760
|
+
actor_id: resolved.identity.agent_id,
|
|
761
|
+
}, cwd);
|
|
762
|
+
}
|
|
763
|
+
else if (assignment.status !== 'started') {
|
|
764
|
+
return { response: createToolErrorResponse('operation_error', `Logical Assignment ${assignmentId} is ${assignment.status}, expected accepted/started`) };
|
|
765
|
+
}
|
|
766
|
+
const { transitionAgentRun } = await import('../core/agentruns.js');
|
|
767
|
+
transitionAgentRun(generation.run_id, 'running', {
|
|
768
|
+
actor: callerAgent,
|
|
769
|
+
actor_id: resolved.identity.agent_id,
|
|
770
|
+
session_id: effectiveSessionId,
|
|
771
|
+
}, cwd);
|
|
772
|
+
return {
|
|
773
|
+
response: toolResponse({
|
|
774
|
+
content: [{ type: 'text', text: `Attempt generation ${generation.attempt_epoch} started for logical Assignment ${assignmentId}` }],
|
|
775
|
+
assignment_id: assignmentId,
|
|
776
|
+
status: 'started',
|
|
777
|
+
run_id: generation.run_id,
|
|
778
|
+
attempt_epoch: generation.attempt_epoch,
|
|
779
|
+
}),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const { recordAgentRunProgress } = await import('../core/agentruns.js');
|
|
783
|
+
recordAgentRunProgress(generation.run_id, {
|
|
784
|
+
message,
|
|
785
|
+
artifacts,
|
|
786
|
+
actor: callerAgent,
|
|
787
|
+
actor_id: resolved.identity.agent_id,
|
|
788
|
+
session_id: effectiveSessionId,
|
|
789
|
+
}, cwd);
|
|
790
|
+
}
|
|
664
791
|
if (status === 'progress') {
|
|
665
792
|
const updated = recordProg(assignmentId, {
|
|
666
793
|
message,
|