brainclaw 1.26.2 → 1.27.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 +87 -14
- package/dist/commands/mcp-catalog.js +42 -18
- package/dist/commands/mcp-schemas.generated.js +44 -0
- package/dist/commands/mcp-write-claims.js +128 -1
- package/dist/commands/mcp-write-coordination.js +146 -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 +160 -14
- package/dist/core/execution-contract.js +345 -0
- package/dist/core/execution.js +130 -16
- 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 +207 -0
- package/dist/core/loops/artifact-contract.js +11 -0
- package/dist/core/loops/attempt-authority.js +476 -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/evidence.js +187 -0
- package/dist/core/loops/facade-schema.js +41 -10
- package/dist/core/loops/gate-policy.js +485 -0
- package/dist/core/loops/impl-bind.js +37 -79
- 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 +235 -18
- package/dist/core/loops/result-reducers.js +99 -10
- package/dist/core/loops/store.js +30 -3
- package/dist/core/loops/turn-execution.js +480 -0
- package/dist/core/loops/types.js +113 -2
- package/dist/core/loops/verbs.js +332 -99
- package/dist/core/loops/verify-command.js +31 -8
- 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 +35 -0
- package/dist/core/spawn-check.js +3 -2
- package/dist/core/upgrades/backup.js +27 -4
- package/dist/facts.js +7 -6
- package/dist/facts.json +6 -5
- 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 +154 -0
- package/docs/loops/research.md +136 -0
- package/docs/loops/review.md +200 -0
- package/docs/mcp-schema-changelog.md +14 -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
|
|
@@ -115,12 +116,11 @@ const SLOT_BOUND_INTENTS = new Set(['complete_turn']);
|
|
|
115
116
|
* took over" window — the verb will not proceed if the lock's mutation_id
|
|
116
117
|
* changed between `acquireLock` and `work` dispatch. It does NOT cover mid-verb
|
|
117
118
|
* 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.
|
|
119
|
+
* atomic-rename + JSONL append without consulting the fence. Safety therefore
|
|
120
|
+
* depends on lock.ts refusing deadline/lease-based takeover of a still-live
|
|
121
|
+
* local process (and failing closed for remote-host owners). If a future slice
|
|
122
|
+
* adds async work inside a mutation or enables time-based/remote takeover,
|
|
123
|
+
* `fenceCheck` must first be threaded to every committing write.
|
|
124
124
|
*/
|
|
125
125
|
function withLockedLoopMutation(req, agentId, cwd, work) {
|
|
126
126
|
return withLoopLock({
|
|
@@ -128,7 +128,7 @@ function withLockedLoopMutation(req, agentId, cwd, work) {
|
|
|
128
128
|
intent: req.intent,
|
|
129
129
|
agentId,
|
|
130
130
|
scope: { kind: 'loop', loopId: req.loop_id },
|
|
131
|
-
expectedVersion: req.expected_version,
|
|
131
|
+
expectedVersion: 'expected_version' in req ? req.expected_version : undefined,
|
|
132
132
|
clientRequestId: req.client_request_id,
|
|
133
133
|
requestPayload: requestPayload(req),
|
|
134
134
|
currentVersion: () => currentLoopVersion(req.loop_id, cwd),
|
|
@@ -295,6 +295,45 @@ export async function handleBclawLoop(options) {
|
|
|
295
295
|
return successResponse('list', { loops: sliced, total: loops.length }, sliced.map((l) => loopArtifactEntry(l.id)), [], [], Date.now() - startMs, `✔ list ${sliced.length}/${loops.length} loops`);
|
|
296
296
|
}
|
|
297
297
|
case 'turn': {
|
|
298
|
+
if (req.dispatch) {
|
|
299
|
+
if (!req.slot_id) {
|
|
300
|
+
return errorResponse('turn', 'validation_error', 'turn dispatch requires slot_id', Date.now() - startMs);
|
|
301
|
+
}
|
|
302
|
+
const dispatched = await dispatchLoopTurn({
|
|
303
|
+
loop_id: req.loop_id,
|
|
304
|
+
slot_id: req.slot_id,
|
|
305
|
+
task: req.input ?? `Execute ${req.loop_id} slot ${req.slot_id}`,
|
|
306
|
+
dispatcher_agent: actor,
|
|
307
|
+
dispatcher_agent_id: req.agentId,
|
|
308
|
+
session_id: options.sessionId,
|
|
309
|
+
model: req.model,
|
|
310
|
+
auto_execute: req.auto_execute,
|
|
311
|
+
candidate_agents: req.target_agents,
|
|
312
|
+
cwd: options.cwd ?? process.cwd(),
|
|
313
|
+
});
|
|
314
|
+
// Before AttemptAuthority exists an error is a true denial. Once the
|
|
315
|
+
// launch grant has crossed, however, transport may fall back to a
|
|
316
|
+
// manual command or become crossed_unknown. Preserve the created
|
|
317
|
+
// entities in a successful structured response instead of reporting
|
|
318
|
+
// an empty-side-effect error that invites a dangerous retry.
|
|
319
|
+
if (dispatched.error && !dispatched.turn_id) {
|
|
320
|
+
return errorResponse('turn', 'dispatch_denied', dispatched.error, Date.now() - startMs);
|
|
321
|
+
}
|
|
322
|
+
const loop = getLoop(req.loop_id, options.cwd);
|
|
323
|
+
return successResponse('turn', { loop, dispatch: dispatched, next_expected: loop ? computeNextExpected(loop) : undefined }, [
|
|
324
|
+
loopArtifactEntry(req.loop_id),
|
|
325
|
+
...(dispatched.assignment_id ? [{ type: 'assignment', id: dispatched.assignment_id }] : []),
|
|
326
|
+
...(dispatched.run_id ? [{ type: 'agent_run', id: dispatched.run_id }] : []),
|
|
327
|
+
...(dispatched.claim_id ? [{ type: 'claim', id: dispatched.claim_id }] : []),
|
|
328
|
+
], [
|
|
329
|
+
sideEffectUpdate('loop', req.loop_id),
|
|
330
|
+
...(dispatched.claim_id ? [{ action: 'create', entity: 'claim', id: dispatched.claim_id }] : []),
|
|
331
|
+
...(dispatched.assignment_id ? [{ action: 'create', entity: 'assignment', id: dispatched.assignment_id }] : []),
|
|
332
|
+
...(dispatched.run_id ? [{ action: 'create', entity: 'agent_run', id: dispatched.run_id }] : []),
|
|
333
|
+
], dispatched.error ? [dispatched.error] : [], Date.now() - startMs, dispatched.error
|
|
334
|
+
? `⚠ ${dispatched.kind}.${dispatched.phase} turn ${dispatched.turn_id} crossed; ${dispatched.error}`
|
|
335
|
+
: `✔ dispatched ${dispatched.kind}.${dispatched.phase} turn ${dispatched.turn_id}`);
|
|
336
|
+
}
|
|
298
337
|
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
299
338
|
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
300
339
|
const loop = turn({
|
|
@@ -316,6 +355,13 @@ export async function handleBclawLoop(options) {
|
|
|
316
355
|
const loop = complete_turn({
|
|
317
356
|
id: req.loop_id,
|
|
318
357
|
slot_id: req.slot_id,
|
|
358
|
+
assignment_id: req.assignment_id,
|
|
359
|
+
turn_id: req.turn_id,
|
|
360
|
+
run_id: req.run_id,
|
|
361
|
+
nonce: req.nonce,
|
|
362
|
+
attempt_epoch: req.attempt_epoch,
|
|
363
|
+
execution_contract_hash: req.execution_contract_hash,
|
|
364
|
+
workspace_digest: req.workspace_digest,
|
|
319
365
|
outcome: req.outcome,
|
|
320
366
|
failure_reason: req.failure_reason,
|
|
321
367
|
artifact: req.artifact
|
|
@@ -337,6 +383,32 @@ export async function handleBclawLoop(options) {
|
|
|
337
383
|
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
384
|
});
|
|
339
385
|
}
|
|
386
|
+
case 'takeover': {
|
|
387
|
+
const authorityHome = readLocalAuthorityHome(options.cwd ?? process.cwd());
|
|
388
|
+
if (!authorityHome) {
|
|
389
|
+
return errorResponse('takeover', 'authority_home_unavailable', 'local store/device authority identity is not initialized', Date.now() - startMs);
|
|
390
|
+
}
|
|
391
|
+
const result = takeoverLoopAttempt({
|
|
392
|
+
loop_id: req.loop_id,
|
|
393
|
+
slot_id: req.slot_id,
|
|
394
|
+
turn_id: req.turn_id,
|
|
395
|
+
expected_epoch: req.expected_epoch,
|
|
396
|
+
authority_home: authorityHome,
|
|
397
|
+
actor,
|
|
398
|
+
actor_id: agentId,
|
|
399
|
+
writer_id: agentId,
|
|
400
|
+
cause: req.cause,
|
|
401
|
+
liveness_evidence: req.liveness_evidence,
|
|
402
|
+
external_effect_policy: req.external_effect_policy,
|
|
403
|
+
next_workspace_path: req.next_workspace_path,
|
|
404
|
+
mode: req.takeover_mode,
|
|
405
|
+
cwd: options.cwd ?? process.cwd(),
|
|
406
|
+
});
|
|
407
|
+
return successResponse('takeover', {
|
|
408
|
+
...result,
|
|
409
|
+
next_action: 'dispatch the same logical turn; the common path will project and contend on launch(next_epoch)',
|
|
410
|
+
}, [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)`);
|
|
411
|
+
}
|
|
340
412
|
case 'advance': {
|
|
341
413
|
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
342
414
|
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
@@ -360,7 +432,6 @@ export async function handleBclawLoop(options) {
|
|
|
360
432
|
phase: req.artifact.phase,
|
|
361
433
|
type: req.artifact.type,
|
|
362
434
|
body: req.artifact.body,
|
|
363
|
-
produced_by: req.artifact.produced_by,
|
|
364
435
|
ref: req.artifact.ref,
|
|
365
436
|
addresses_critique: req.artifact.addresses_critique,
|
|
366
437
|
},
|
|
@@ -467,10 +538,9 @@ export async function handleBclawLoop(options) {
|
|
|
467
538
|
}, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summary);
|
|
468
539
|
}
|
|
469
540
|
case 'bind': {
|
|
470
|
-
//
|
|
471
|
-
// bind
|
|
472
|
-
//
|
|
473
|
-
// coordinate(open_loop)'s async-handler-spawns pattern, not a synchronous verb.
|
|
541
|
+
// Implementation bind is engine-only: validate the linked sequence and
|
|
542
|
+
// advance bind -> execute. Worker launch belongs exclusively to
|
|
543
|
+
// turn(dispatch=true), the common AttemptAuthority path.
|
|
474
544
|
const existing = getLoop(req.loop_id, options.cwd);
|
|
475
545
|
if (!existing) {
|
|
476
546
|
return errorResponse('bind', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
@@ -504,7 +574,7 @@ export async function handleBclawLoop(options) {
|
|
|
504
574
|
dispatched: bind.messages_sent,
|
|
505
575
|
dispatch: bind.dispatch,
|
|
506
576
|
next_expected: computeNextExpected(loop),
|
|
507
|
-
}, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], sideEffects, bind.dispatch?.warnings ?? [], Date.now() - startMs, bind.reason);
|
|
577
|
+
}, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], sideEffects, [...bind.warnings, ...(bind.dispatch?.warnings ?? [])], Date.now() - startMs, bind.reason);
|
|
508
578
|
}
|
|
509
579
|
}
|
|
510
580
|
}
|
|
@@ -538,6 +608,9 @@ export async function handleBclawLoop(options) {
|
|
|
538
608
|
});
|
|
539
609
|
}
|
|
540
610
|
const message = err instanceof Error ? err.message : String(err);
|
|
611
|
+
if (message.startsWith('attempt_fence_')) {
|
|
612
|
+
return errorResponse(req.intent, 'attempt_fence_rejected', message, Date.now() - startMs);
|
|
613
|
+
}
|
|
541
614
|
if (message.includes('unauthorized_slot_write')) {
|
|
542
615
|
return errorResponse(req.intent, 'unauthorized_slot_write', message, Date.now() - startMs);
|
|
543
616
|
}
|
|
@@ -872,7 +872,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
872
872
|
},
|
|
873
873
|
{
|
|
874
874
|
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).
|
|
875
|
+
description: 'Loop engine facade: open/turn/complete_turn/takeover/advance/add_artifact/pause/resume/close/verify/request_input/provide_input/get/list multi-turn work loops (review, ideation, implementation, research, debug). 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
876
|
// schemaSource is informational for now — grep target so future migrators
|
|
877
877
|
// can locate zod-derived tools quickly. The parity test in
|
|
878
878
|
// tests/unit/mcp-zod-parity.test.ts hard-codes its (tool, zod-schema)
|
|
@@ -890,12 +890,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
890
890
|
properties: {
|
|
891
891
|
intent: {
|
|
892
892
|
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.',
|
|
893
|
+
enum: ['open', 'get', 'list', 'turn', 'complete_turn', 'takeover', 'advance', 'add_artifact', 'pause', 'resume', 'close', 'verify', 'bind', 'request_input', 'provide_input'],
|
|
894
|
+
description: 'Loop lifecycle intent. Review/ideation normally start via bclaw_coordinate; implementation/research/debug may use open with allow_orphan=true and then explicitly bind/turn/dispatch. `verify` runs the configured verification; request_input/provide_input are cross-kind clarification primitives.',
|
|
899
895
|
},
|
|
900
896
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
901
897
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -906,28 +902,56 @@ const MCP_WRITE_TOOLS = [
|
|
|
906
902
|
linked: { type: 'object', description: 'Optional top-level plan/sequence refs (open).' },
|
|
907
903
|
stop_condition: { type: 'object', description: 'Optional stop_condition override (open). Composite any/all supported.' },
|
|
908
904
|
mode: { type: 'string', enum: ['asymmetric', 'symmetric'], description: 'Review mode selector for open (review kind only).' },
|
|
905
|
+
allow_orphan: { type: 'boolean', description: 'Required true for direct open: acknowledges that the caller owns subsequent turn/bind/dispatch.' },
|
|
906
|
+
verify: { type: 'object', description: 'Optional open-time verification policy, e.g. { command: ["npm", "test"] }.' },
|
|
909
907
|
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
908
|
include_events: { type: 'boolean', description: 'get: include the event journal in the response.' },
|
|
911
909
|
limit: { type: 'number', description: 'list: max loops returned.' },
|
|
912
910
|
offset: { type: 'number', description: 'list: pagination offset.' },
|
|
913
|
-
slot_id: { type: 'string', description: 'Slot id for turn / complete_turn.' },
|
|
911
|
+
slot_id: { type: 'string', description: 'Slot id for turn / complete_turn / takeover.' },
|
|
912
|
+
turn_id: { type: 'string', description: 'takeover: stable logical turn id; complete_turn: required as part of the full AttemptAuthority v2 generation fence.' },
|
|
913
|
+
expected_epoch: { type: 'number', description: 'takeover: active physical generation epoch expected by the caller.' },
|
|
914
|
+
cause: { type: 'string', description: 'takeover: audited reason for fencing the current producer.' },
|
|
915
|
+
liveness_evidence: { type: 'string', description: 'takeover: concrete evidence that the current producer cannot safely continue.' },
|
|
916
|
+
external_effect_policy: { type: 'string', enum: ['none', 'idempotent', 'externally_fenced'], description: 'takeover: declaration required before automatic re-execution.' },
|
|
917
|
+
next_workspace_path: { type: 'string', description: 'takeover: existing isolated workspace for the successor generation.' },
|
|
918
|
+
takeover_mode: { type: 'string', enum: ['takeover', 'retry'], description: 'takeover: causal close decision kind (default takeover).' },
|
|
914
919
|
role: { type: 'string', description: 'Slot role for turn (resolves the first non-done slot with that role).' },
|
|
915
920
|
input: { type: 'string', description: 'turn: free-form input passed to the slot.' },
|
|
916
|
-
assignment_id: { type: 'string', description: 'turn: assignment id produced by
|
|
917
|
-
|
|
921
|
+
assignment_id: { type: 'string', description: 'turn: assignment id produced by an external dispatcher; complete_turn: current logical assignment id in the full v2 fence.' },
|
|
922
|
+
run_id: { type: 'string', description: 'complete_turn: current physical AgentRun id in the full AttemptAuthority v2 fence.' },
|
|
923
|
+
nonce: { type: 'string', description: 'complete_turn: current immutable launch nonce in the full AttemptAuthority v2 fence.' },
|
|
924
|
+
attempt_epoch: { type: 'number', description: 'complete_turn: current physical generation epoch in the full AttemptAuthority v2 fence.' },
|
|
925
|
+
execution_contract_hash: { type: 'string', description: 'complete_turn: SHA-256 hash of the current generation ExecutionContract.' },
|
|
926
|
+
workspace_digest: { type: 'string', description: 'complete_turn: digest binding the current generation to its isolated workspace.' },
|
|
927
|
+
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
928
|
outcome: { type: 'string', enum: ['done', 'failed', 'cancelled'], description: 'complete_turn outcome (default done).' },
|
|
919
929
|
failure_reason: { type: 'string', description: 'complete_turn: optional failure/cancel reason.' },
|
|
920
930
|
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
|
-
|
|
931
|
+
dry_run: { type: 'boolean', description: 'bind: validate the linked sequence without advancing; bind never spawns.' },
|
|
932
|
+
lanes: { type: 'array', items: { type: 'string' }, description: 'Deprecated bind launch option retained for compatibility and ignored; dispatch independent slots with turn(dispatch=true).' },
|
|
933
|
+
auto_execute: { type: 'boolean', description: 'turn dispatch: false returns a contract-wrapped manual launch command. Deprecated and ignored for engine-only bind.' },
|
|
934
|
+
model: { type: 'string', description: 'turn dispatch: model override. Deprecated and ignored for engine-only bind.' },
|
|
935
|
+
target_agents: { type: 'array', items: { type: 'string' }, description: 'turn dispatch: deterministic capability candidate pool used when the slot has no frozen agent.' },
|
|
936
|
+
max_assignments: { type: 'number', description: 'Deprecated bind launch option retained for compatibility and ignored.' },
|
|
926
937
|
to_phase: { type: 'string', description: 'advance: explicit target phase (otherwise the next phase).' },
|
|
927
938
|
force: { type: 'boolean', description: 'advance: allow going backwards (increments iteration_count).' },
|
|
928
939
|
reason: { type: 'string', description: 'advance / pause / close: optional reason string.' },
|
|
929
|
-
expected_version: { type: 'number', description: '
|
|
930
|
-
|
|
940
|
+
expected_version: { type: 'number', description: 'Optimistic-CAS version enforced under the loop lock for mutating intents; stale values fail with version_conflict before mutation.' },
|
|
941
|
+
phase: { type: 'string', description: 'request_input: current loop phase.' },
|
|
942
|
+
question_text: { type: 'string', description: 'request_input: operator question (max 500 characters).' },
|
|
943
|
+
evidence: { type: 'array', items: { type: 'string' }, description: 'request_input: concrete evidence motivating the question.' },
|
|
944
|
+
suggested_default: { type: 'string', description: 'request_input: optional default answer.' },
|
|
945
|
+
options: { type: 'array', items: { type: 'object' }, description: 'request_input: optional 2-4 structured choices.' },
|
|
946
|
+
pause_scope: { type: 'string', enum: ['slot', 'loop'], description: 'request_input: pause only the slot or the whole loop.' },
|
|
947
|
+
on_timeout: { type: 'string', enum: ['use_default', 'cancel_loop', 'continue_incomplete'], description: 'request_input: timeout policy.' },
|
|
948
|
+
timeout_at: { type: 'string', description: 'request_input: optional ISO timestamp.' },
|
|
949
|
+
replies_to: { type: 'string', description: 'provide_input: question id (qst_…).' },
|
|
950
|
+
resolved_via: { type: 'string', enum: ['answer', 'choose', 'skip', 'timeout_default'], description: 'provide_input: how the answer was resolved.' },
|
|
951
|
+
answer_text: { type: 'string', description: 'provide_input: free-form answer.' },
|
|
952
|
+
chosen_option_id: { type: 'string', description: 'provide_input: selected option id.' },
|
|
953
|
+
by: { type: 'string', enum: ['operator', 'system'], description: 'provide_input: answer actor (default operator).' },
|
|
954
|
+
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
955
|
project: { type: 'string', description: 'Optional linked project name/path. Routes loop reads and mutations to that project. Defaults to the current cwd.' },
|
|
932
956
|
agent: { type: 'string', description: 'Caller agent name.' },
|
|
933
957
|
agentId: { type: 'string', description: 'Caller registered agent id (enforced for slot-bound auth in complete_turn).' },
|
|
@@ -937,7 +961,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
937
961
|
},
|
|
938
962
|
{
|
|
939
963
|
name: 'bclaw_assignment_update',
|
|
940
|
-
description: 'Report assignment lifecycle status. Part of the Agent SDK runtime protocol.
|
|
964
|
+
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
965
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
942
966
|
inputSchema: { ...generatedSchemas.AssignmentUpdateRequest },
|
|
943
967
|
},
|
|
@@ -650,6 +650,28 @@ export const generatedSchemas = {
|
|
|
650
650
|
"coordinator_override": {
|
|
651
651
|
"type": "boolean",
|
|
652
652
|
"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."
|
|
653
|
+
},
|
|
654
|
+
"turn_id": {
|
|
655
|
+
"type": "string"
|
|
656
|
+
},
|
|
657
|
+
"run_id": {
|
|
658
|
+
"type": "string"
|
|
659
|
+
},
|
|
660
|
+
"nonce": {
|
|
661
|
+
"type": "string"
|
|
662
|
+
},
|
|
663
|
+
"attempt_epoch": {
|
|
664
|
+
"type": "integer",
|
|
665
|
+
"minimum": 0,
|
|
666
|
+
"maximum": 9007199254740991
|
|
667
|
+
},
|
|
668
|
+
"execution_contract_hash": {
|
|
669
|
+
"type": "string",
|
|
670
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
671
|
+
},
|
|
672
|
+
"workspace_digest": {
|
|
673
|
+
"type": "string",
|
|
674
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
653
675
|
}
|
|
654
676
|
},
|
|
655
677
|
"required": [
|
|
@@ -1014,6 +1036,28 @@ export const generatedSchemas = {
|
|
|
1014
1036
|
],
|
|
1015
1037
|
"description": "Optional ActionRequired payload when status=blocked. Lets the worker request approval, user input, or clarification before resuming."
|
|
1016
1038
|
},
|
|
1039
|
+
"turn_id": {
|
|
1040
|
+
"type": "string"
|
|
1041
|
+
},
|
|
1042
|
+
"run_id": {
|
|
1043
|
+
"type": "string"
|
|
1044
|
+
},
|
|
1045
|
+
"nonce": {
|
|
1046
|
+
"type": "string"
|
|
1047
|
+
},
|
|
1048
|
+
"attempt_epoch": {
|
|
1049
|
+
"type": "integer",
|
|
1050
|
+
"minimum": 0,
|
|
1051
|
+
"maximum": 9007199254740991
|
|
1052
|
+
},
|
|
1053
|
+
"execution_contract_hash": {
|
|
1054
|
+
"type": "string",
|
|
1055
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
1056
|
+
},
|
|
1057
|
+
"workspace_digest": {
|
|
1058
|
+
"type": "string",
|
|
1059
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
1060
|
+
},
|
|
1017
1061
|
"agent": {
|
|
1018
1062
|
"type": "string",
|
|
1019
1063
|
"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,
|