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.
Files changed (39) hide show
  1. package/dist/brainclaw-vscode.vsix +0 -0
  2. package/dist/cli/register-coordination.js +12 -0
  3. package/dist/commands/code-map.js +2 -0
  4. package/dist/commands/doctor.js +1 -0
  5. package/dist/commands/harvest.js +32 -43
  6. package/dist/commands/loop.js +12 -0
  7. package/dist/commands/loops-handlers.js +284 -17
  8. package/dist/commands/mcp-catalog.js +6 -3
  9. package/dist/commands/mcp-write-claims.js +55 -8
  10. package/dist/commands/mcp-write-coordination.js +413 -137
  11. package/dist/commands/mcp.js +32 -4
  12. package/dist/core/actions.js +17 -3
  13. package/dist/core/agentrun-reconciler.js +138 -4
  14. package/dist/core/claims.js +4 -1
  15. package/dist/core/code-map/backend.js +8 -0
  16. package/dist/core/execution-adapters.js +15 -7
  17. package/dist/core/hygiene-policy.js +2 -1
  18. package/dist/core/loop-turn-dispatch.js +18 -1
  19. package/dist/core/loops/attempt-authority.js +22 -4
  20. package/dist/core/loops/attempt-generations.js +17 -4
  21. package/dist/core/loops/attempt-reservation.js +14 -1
  22. package/dist/core/loops/attempt-takeover.js +173 -76
  23. package/dist/core/loops/continuation.js +337 -0
  24. package/dist/core/loops/facade-schema.js +15 -0
  25. package/dist/core/loops/index.js +1 -0
  26. package/dist/core/loops/reconcile-turn.js +224 -26
  27. package/dist/core/loops/result-reducers.js +8 -8
  28. package/dist/core/loops/turn-execution.js +38 -19
  29. package/dist/core/loops/types.js +9 -0
  30. package/dist/core/loops/verbs.js +1 -1
  31. package/dist/core/reviewer-policy.js +39 -0
  32. package/dist/core/schema.js +16 -1
  33. package/dist/facts.js +8 -8
  34. package/dist/facts.json +7 -7
  35. package/docs/cli.md +4 -2
  36. package/docs/code-map.md +10 -0
  37. package/docs/concepts/loop-engine.md +30 -0
  38. package/docs/mcp-schema-changelog.md +6 -1
  39. package/package.json +1 -1
@@ -13,22 +13,23 @@ import { getLoop } from './store.js';
13
13
  import { phasePolicy } from './kind-policies.js';
14
14
  /** Idempotently materialize every child projection required before launch. */
15
15
  export function ensureTurnExecutionProjections(reservation, input, cwd) {
16
- if (reservation.claim_id === '')
16
+ const claimId = input.claim_id ?? reservation.claim_id;
17
+ if (claimId === '')
17
18
  throw new Error('attempt reservation has no claim');
18
19
  ensureAssignmentProjection({
19
20
  id: input.assignment_id,
20
21
  short_label: input.assignment_id,
21
- claim_id: reservation.claim_id,
22
+ claim_id: claimId,
22
23
  agent: input.agent,
23
24
  agent_id: input.agent_id,
24
25
  dispatcher_agent: input.dispatcher_agent,
25
26
  dispatcher_session_id: input.dispatcher_session_id,
26
27
  scope: input.scope,
27
28
  description: input.description,
28
- // Assignment is the stable logical attempt. Its contract projection stays
29
- // generation-zero; each physical AgentRun below carries its own contract.
30
- execution_contract_ref: reservation.execution_contract_ref,
31
- capability_snapshot: reservation.capability_snapshot,
29
+ // Assignment is generation-scoped. Generation zero preserves the historic
30
+ // deterministic id; every takeover projects a fresh assignment and contract.
31
+ execution_contract_ref: input.execution_contract_ref ?? reservation.execution_contract_ref,
32
+ capability_snapshot: input.capability_snapshot ?? reservation.capability_snapshot,
32
33
  tags: input.assignment_tags ?? ['coordinate', 'loop', 'turn-owned'],
33
34
  }, cwd);
34
35
  input.on_projection?.('assignment');
@@ -36,7 +37,7 @@ export function ensureTurnExecutionProjections(reservation, input, cwd) {
36
37
  id: input.run_id,
37
38
  short_label: input.run_id,
38
39
  assignment_id: input.assignment_id,
39
- claim_id: reservation.claim_id,
40
+ claim_id: claimId,
40
41
  attempt_index: input.attempt_index ?? 1,
41
42
  agent: input.agent,
42
43
  agent_id: input.agent_id,
@@ -50,7 +51,7 @@ export function ensureTurnExecutionProjections(reservation, input, cwd) {
50
51
  tags: input.run_tags ?? ['turn-owned', 'loop'],
51
52
  }, cwd);
52
53
  input.on_projection?.('run');
53
- ensureClaimAssignmentBinding(reservation.claim_id, input.assignment_id, cwd, {
54
+ ensureClaimAssignmentBinding(claimId, input.assignment_id, cwd, {
54
55
  worktreePath: input.worktree_path,
55
56
  });
56
57
  input.on_projection?.('claim_binding');
@@ -61,7 +62,7 @@ export function ensureTurnExecutionProjections(reservation, input, cwd) {
61
62
  input: input.task,
62
63
  turn_id: input.turn_id,
63
64
  assignment_id: input.assignment_id,
64
- claim_id: reservation.claim_id,
65
+ claim_id: claimId,
65
66
  }, cwd);
66
67
  input.on_projection?.('slot_binding');
67
68
  input.on_projection?.('before_crossing');
@@ -71,7 +72,11 @@ function preconditionDenied(reason) {
71
72
  }
72
73
  function authorityDenied(input, turnId, reason) {
73
74
  const reservation = getReservation(turnId, input.cwd);
74
- const ownsAuthority = reservation?.claim_id === input.claim_id;
75
+ const chain = resolveTurnGenerationChain(input.cwd, turnId);
76
+ const authorityClaimId = chain?.status === 'active'
77
+ ? chain.latest_generation.executor?.claim_id ?? reservation?.claim_id
78
+ : reservation?.claim_id;
79
+ const ownsAuthority = authorityClaimId === input.claim_id;
75
80
  const crossed = reservation?.launch?.status === 'crossed';
76
81
  return {
77
82
  kind: 'denied',
@@ -129,13 +134,19 @@ export function prepareTurnExecution(input) {
129
134
  }, input.cwd);
130
135
  const childIds = deriveChildIds(turnId);
131
136
  const existingReservation = getReservation(turnId, input.cwd);
137
+ const v2 = resolveTurnGenerationChain(input.cwd, turnId);
138
+ const activeExecutor = v2?.status === 'active' && v2.latest_generation.attempt_epoch > 0
139
+ ? v2.latest_generation.executor
140
+ : undefined;
132
141
  if (slot.agent !== undefined && slot.agent !== input.agent) {
133
142
  return preconditionDenied(`slot ${input.slot_id} belongs to agent '${slot.agent}', not '${input.agent}'`);
134
143
  }
135
144
  if (slot.agent_id !== undefined && slot.agent_id !== input.agent_id) {
136
145
  return preconditionDenied(`slot ${input.slot_id} belongs to agent_id '${slot.agent_id}', not '${input.agent_id ?? 'none'}'`);
137
146
  }
138
- if (slot.claim_id !== undefined && slot.claim_id !== input.claim_id) {
147
+ if (slot.claim_id !== undefined
148
+ && slot.claim_id !== input.claim_id
149
+ && ['assigned', 'working', 'waiting_input'].includes(slot.status)) {
139
150
  return preconditionDenied(`slot ${input.slot_id} is bound to claim ${slot.claim_id}, not ${input.claim_id}`);
140
151
  }
141
152
  if (slot.current_turn_id !== undefined
@@ -202,13 +213,15 @@ export function prepareTurnExecution(input) {
202
213
  catch (error) {
203
214
  return preconditionDenied(error instanceof Error ? error.message : String(error));
204
215
  }
205
- const frozenHarnessBinding = existingReservation?.capability_snapshot?.resolved.harness;
216
+ const frozenHarnessBinding = activeExecutor?.capability_snapshot.resolved.harness
217
+ ?? existingReservation?.capability_snapshot?.resolved.harness;
206
218
  if (frozenHarnessBinding && JSON.stringify(frozenHarnessBinding) !== JSON.stringify(requestedHarnessBinding)) {
207
219
  return preconditionDenied(`harness binding differs from immutable capability snapshot: frozen `
208
220
  + `${frozenHarnessBinding.adapter_id}@${frozenHarnessBinding.adapter_version}, requested `
209
221
  + `${requestedHarnessBinding.adapter_id}@${requestedHarnessBinding.adapter_version}`);
210
222
  }
211
- const capabilitySnapshot = existingReservation?.capability_snapshot
223
+ const capabilitySnapshot = activeExecutor?.capability_snapshot
224
+ ?? existingReservation?.capability_snapshot
212
225
  ?? resolveCapabilitySnapshot(input.agent, capabilityRequirement, input.agent_id, requestedHarnessBinding);
213
226
  if (!capabilitySnapshot.accepted) {
214
227
  const reasons = capabilitySnapshot.reasons.map((reason) => reason.code).join(', ');
@@ -256,7 +269,6 @@ export function prepareTurnExecution(input) {
256
269
  const contractRef = contract
257
270
  ? (existingReservation?.execution_contract_ref ?? executionContractRef(contract, capabilitySnapshot))
258
271
  : undefined;
259
- const v2 = resolveTurnGenerationChain(input.cwd, turnId);
260
272
  const activeRollout = resolveActiveAttemptRollout(input.cwd);
261
273
  const localHome = readLocalAuthorityHome(input.cwd);
262
274
  if (activeRollout && (!localHome || !contractRef)) {
@@ -339,10 +351,16 @@ export function prepareTurnExecution(input) {
339
351
  try {
340
352
  const generation = v2.latest_generation;
341
353
  const generationContract = executionContractForGeneration(reservation, generation);
354
+ const executor = generation.executor ?? {
355
+ agent: reservation.agent,
356
+ agent_id: reservation.agent_id,
357
+ claim_id: reservation.claim_id,
358
+ capability_snapshot: reservation.capability_snapshot,
359
+ };
342
360
  let accepted;
343
361
  try {
344
362
  accepted = input.accepted_execution_contract
345
- ?? attestHarnessContractAcceptance(generationContract.ref, reservation.capability_snapshot, requestedHarnessBinding);
363
+ ?? attestHarnessContractAcceptance(generationContract.ref, executor.capability_snapshot, requestedHarnessBinding);
346
364
  }
347
365
  catch (error) {
348
366
  return preconditionDenied(`worker contract acceptance unavailable before crossing: ${error instanceof Error ? error.message : String(error)}`);
@@ -357,8 +375,9 @@ export function prepareTurnExecution(input) {
357
375
  turn_id: turnId,
358
376
  assignment_id: generation.assignment_id,
359
377
  run_id: generation.run_id,
360
- agent: input.agent,
361
- agent_id: input.agent_id,
378
+ claim_id: executor.claim_id,
379
+ agent: executor.agent,
380
+ agent_id: executor.agent_id,
362
381
  dispatcher_agent: input.dispatcher_agent,
363
382
  dispatcher_agent_id: input.dispatcher_agent_id,
364
383
  dispatcher_session_id: input.dispatcher_session_id,
@@ -370,7 +389,7 @@ export function prepareTurnExecution(input) {
370
389
  run_tags: [...(input.run_tags ?? ['turn-owned', 'loop']), `attempt-generation:${generation.attempt_epoch}`],
371
390
  attempt_index: generation.attempt_epoch + 1,
372
391
  execution_contract_ref: generationContract.ref,
373
- capability_snapshot: reservation.capability_snapshot,
392
+ capability_snapshot: executor.capability_snapshot,
374
393
  on_projection: input.on_projection,
375
394
  }, input.cwd);
376
395
  const crossing = crossActiveAttemptGenerationV2(turnId, generation.attempt_epoch, localHome, input.dispatcher_agent_id ?? input.dispatcher_agent, input.dispatcher_agent_id ?? input.dispatcher_agent, input.cwd);
@@ -387,7 +406,7 @@ export function prepareTurnExecution(input) {
387
406
  workspace_path: generation.workspace_path,
388
407
  contract_status: 'contracted',
389
408
  execution_contract_ref: generationContract.ref,
390
- capability_snapshot: reservation.capability_snapshot,
409
+ capability_snapshot: executor.capability_snapshot,
391
410
  };
392
411
  }
393
412
  catch (error) {
@@ -25,6 +25,12 @@ export const LoopLinksSchema = z.object({
25
25
  sequence_ids: z.array(z.string().min(1)).optional(),
26
26
  /** Upstream loop in an ideation → implementation → review pipeline. */
27
27
  source_loop_id: z.string().regex(/^lop_[0-9a-z]+$/).optional(),
28
+ /** Exact upstream artifact that authorized this continuation. */
29
+ source_artifact_id: z.string().regex(/^art_[0-9a-z]+$/).optional(),
30
+ /** Sealed digest of source_artifact_id at continuation evaluation time. */
31
+ source_artifact_digest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
32
+ /** Durable, deterministic identity of the policy decision that created this loop. */
33
+ continuation_key: z.string().regex(/^[a-f0-9]{64}$/).optional(),
28
34
  });
29
35
  /**
30
36
  * Memory categories a loop phase can request via `context_filter` (pln#492).
@@ -728,6 +734,9 @@ export const LoopEventSchema = z.discriminatedUnion('kind', [
728
734
  slot_id: z.string().min(1),
729
735
  turn_id: z.string().min(1),
730
736
  assignment_id: z.string().min(1),
737
+ claim_id: z.string().min(1).optional(),
738
+ agent: z.string().min(1).optional(),
739
+ agent_id: z.string().min(1).optional(),
731
740
  from_epoch: z.number().int().nonnegative(),
732
741
  to_epoch: z.number().int().positive(),
733
742
  from_run_id: z.string().min(1),
@@ -586,7 +586,7 @@ function authorizeCompleteTurnAttempt(input, slot, cwd) {
586
586
  slot_role: slot.role,
587
587
  turn_id: generation.turn_id,
588
588
  assignment_id: generation.assignment_id,
589
- claim_id: reservation.claim_id ?? slot.claim_id,
589
+ claim_id: generation.executor?.claim_id ?? slot.claim_id ?? reservation.claim_id,
590
590
  run_id: generation.run_id,
591
591
  nonce: generation.launch_nonce,
592
592
  attempt_epoch: generation.attempt_epoch,
@@ -0,0 +1,39 @@
1
+ import { listAgentIdentities } from './agent-registry.js';
2
+ import { resolveExecutionCandidate } from './execution-contract.js';
3
+ export const REVIEWER_SELECTION_POLICY_VERSION = 'reviewer-selection-v1';
4
+ /**
5
+ * Select a concrete review worker from project-registered identities.
6
+ *
7
+ * The shared execution-contract resolver supplies capability checks and stable
8
+ * ordering. The policy additionally enforces reviewer independence by
9
+ * excluding every identity frozen onto an implementation slot.
10
+ */
11
+ export function selectImplementationReviewer(source, cwd) {
12
+ if (source.kind !== 'implementation') {
13
+ throw new Error(`reviewer_selection_source_invalid: loop ${source.id} is ${source.kind}`);
14
+ }
15
+ const excludedImplementers = source.slots
16
+ .filter((slot) => Boolean(slot.agent))
17
+ .map((slot) => ({ agent: slot.agent, ...(slot.agent_id ? { agent_id: slot.agent_id } : {}) }));
18
+ const excludedNames = new Set(excludedImplementers.map((identity) => identity.agent.normalize('NFC')));
19
+ const excludedIds = new Set(excludedImplementers.flatMap((identity) => identity.agent_id ? [identity.agent_id.normalize('NFC')] : []));
20
+ const identities = listAgentIdentities(cwd)
21
+ .filter((identity) => identity.kind !== 'human')
22
+ .filter((identity) => !excludedNames.has(identity.agent_name.normalize('NFC')) && !excludedIds.has(identity.agent_id.normalize('NFC')))
23
+ .map((identity) => ({ agent: identity.agent_name, agent_id: identity.agent_id }));
24
+ const resolution = resolveExecutionCandidate(identities, { roles: ['review'], required_surfaces: ['cli_spawn'], execution_surfaces: [], required_tools: [] });
25
+ if (resolution.kind !== 'selected') {
26
+ const reasons = resolution.evaluated
27
+ .map((candidate) => `${candidate.agent}:${candidate.snapshot.reasons.map((reason) => reason.code).join('+') || 'excluded'}`)
28
+ .join(', ');
29
+ throw new Error(`continuation_reviewer_unavailable: no independent spawnable reviewer${reasons ? ` (${reasons})` : ''}`);
30
+ }
31
+ return {
32
+ policy_version: REVIEWER_SELECTION_POLICY_VERSION,
33
+ agent: resolution.selected.agent,
34
+ agent_id: resolution.selected.agent_id,
35
+ evaluated: resolution.evaluated,
36
+ excluded_implementers: excludedImplementers,
37
+ };
38
+ }
39
+ //# sourceMappingURL=reviewer-policy.js.map
@@ -923,11 +923,18 @@ export const ActionRequiredResponseSchema = z.object({
923
923
  responded_by_id: z.string().optional(),
924
924
  responded_at: z.string(),
925
925
  });
926
+ export const ActionRequiredTargetSchema = z.discriminatedUnion('kind', [
927
+ z.object({ kind: z.literal('assignment'), assignment_id: z.string() }),
928
+ z.object({ kind: z.literal('continuation'), continuation_id: z.string().regex(/^ctn_[a-f0-9]{24}$/) }),
929
+ ]);
926
930
  export const ActionRequiredSchema = z.object({
927
931
  schema_version: z.number().int().positive().optional(),
928
932
  id: z.string(),
929
933
  short_label: z.string().optional(),
930
- assignment_id: z.string(),
934
+ /** Legacy top-level assignment link; retained for v1 records. */
935
+ assignment_id: z.string().optional(),
936
+ /** Discriminated approval target. New records always persist this field. */
937
+ target: ActionRequiredTargetSchema.optional(),
931
938
  run_id: z.string().optional(),
932
939
  claim_id: z.string().optional(),
933
940
  message_id: z.string().optional(),
@@ -949,6 +956,14 @@ export const ActionRequiredSchema = z.object({
949
956
  resolved_at: z.string().optional(),
950
957
  response: ActionRequiredResponseSchema.optional(),
951
958
  tags: TagsWithDefaultSchema,
959
+ }).superRefine((action, ctx) => {
960
+ const target = action.target ?? (action.assignment_id ? { kind: 'assignment', assignment_id: action.assignment_id } : undefined);
961
+ if (!target) {
962
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['target'], message: 'ActionRequired requires an assignment or continuation target' });
963
+ }
964
+ if (target?.kind === 'assignment' && action.assignment_id && target.assignment_id !== action.assignment_id) {
965
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['target'], message: 'assignment target must match assignment_id' });
966
+ }
952
967
  });
953
968
  // --- Runtime notes schemas ---
954
969
  export const RuntimeNoteTypeSchema = z.enum(['observation', 'session_start', 'session_end']);
package/dist/facts.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
2
- // Source: brainclaw v1.28.0 on 2026-08-24T07:09:07.814Z
2
+ // Source: brainclaw v1.28.2 on 2026-08-25T18:22:09.950Z
3
3
  export const FACTS = {
4
- "version": "1.28.0",
5
- "generated_at": "2026-08-24T07:09:07.814Z",
4
+ "version": "1.28.2",
5
+ "generated_at": "2026-08-25T18:22:09.950Z",
6
6
  "tools": {
7
7
  "count": 70,
8
8
  "published_count": 68,
@@ -478,7 +478,7 @@ export const FACTS = {
478
478
  },
479
479
  "bench": {
480
480
  "schema": "brainclaw.bench.v1",
481
- "generated_at": "2026-08-24T07:09:05.728Z",
481
+ "generated_at": "2026-08-25T18:22:07.761Z",
482
482
  "node_version": "v24.19.0",
483
483
  "platform": "linux-x64",
484
484
  "repeats": 3,
@@ -487,7 +487,7 @@ export const FACTS = {
487
487
  "name": "cold_onboard",
488
488
  "volume": "empty",
489
489
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
490
- "duration_ms_median": 81,
490
+ "duration_ms_median": 82,
491
491
  "payload_chars_median": 1640,
492
492
  "payload_tokens_est_median": 410
493
493
  },
@@ -495,7 +495,7 @@ export const FACTS = {
495
495
  "name": "warm_work",
496
496
  "volume": "medium",
497
497
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
498
- "duration_ms_median": 120,
498
+ "duration_ms_median": 132,
499
499
  "payload_chars_median": 2626,
500
500
  "payload_tokens_est_median": 657
501
501
  },
@@ -504,8 +504,8 @@ export const FACTS = {
504
504
  "volume": "medium",
505
505
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
506
506
  "duration_ms_median": 13,
507
- "payload_chars_median": 1305,
508
- "payload_tokens_est_median": 326
507
+ "payload_chars_median": 1629,
508
+ "payload_tokens_est_median": 407
509
509
  }
510
510
  ]
511
511
  }
package/dist/facts.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.28.0",
3
- "generated_at": "2026-08-24T07:09:07.814Z",
2
+ "version": "1.28.2",
3
+ "generated_at": "2026-08-25T18:22:09.950Z",
4
4
  "tools": {
5
5
  "count": 70,
6
6
  "published_count": 68,
@@ -476,7 +476,7 @@
476
476
  },
477
477
  "bench": {
478
478
  "schema": "brainclaw.bench.v1",
479
- "generated_at": "2026-08-24T07:09:05.728Z",
479
+ "generated_at": "2026-08-25T18:22:07.761Z",
480
480
  "node_version": "v24.19.0",
481
481
  "platform": "linux-x64",
482
482
  "repeats": 3,
@@ -485,7 +485,7 @@
485
485
  "name": "cold_onboard",
486
486
  "volume": "empty",
487
487
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
488
- "duration_ms_median": 81,
488
+ "duration_ms_median": 82,
489
489
  "payload_chars_median": 1640,
490
490
  "payload_tokens_est_median": 410
491
491
  },
@@ -493,7 +493,7 @@
493
493
  "name": "warm_work",
494
494
  "volume": "medium",
495
495
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
496
- "duration_ms_median": 120,
496
+ "duration_ms_median": 132,
497
497
  "payload_chars_median": 2626,
498
498
  "payload_tokens_est_median": 657
499
499
  },
@@ -502,8 +502,8 @@
502
502
  "volume": "medium",
503
503
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
504
504
  "duration_ms_median": 13,
505
- "payload_chars_median": 1305,
506
- "payload_tokens_est_median": 326
505
+ "payload_chars_median": 1629,
506
+ "payload_tokens_est_median": 407
507
507
  }
508
508
  ]
509
509
  }
package/docs/cli.md CHANGED
@@ -1016,9 +1016,11 @@ research, and debug; they are not a review-only command group.
1016
1016
  | `takeover <loop_id>` | slot, turn, expected epoch, cause, liveness evidence, external-effect policy, next workspace and coordinator identity | Fence one physical generation and arm a successor without changing the logical Assignment. |
1017
1017
  | `advance <loop_id>` | — | Advance through the protocol; optional `--to-phase`, `--force`, `--reason`. |
1018
1018
  | `add-artifact <loop_id>` | `--phase --type --body` | Attach a typed artifact; optional producer and ref. |
1019
+ | `continue <loop_id>` | — | Evaluate an attested Ideation→Implementation or Implementation→Review action and persist/apply `AUTO`, `REQUIRE_APPROVAL`, or `DENY`; options: `--action-index`, `--autonomy-mode`, `--risk`. Review continuation selects an independent registered reviewer and fails closed if none is available. |
1019
1020
 
1020
1021
  ```bash
1021
1022
  brainclaw loop advance lop_abc --json
1023
+ brainclaw loop continue lop_abc --autonomy-mode autonomous --risk normal --json
1022
1024
  brainclaw loop takeover lop_abc \
1023
1025
  --slot lsl_abc --turn-id tat_abc --expected-epoch 0 \
1024
1026
  --cause "worker is no longer live" \
@@ -1028,7 +1030,7 @@ brainclaw loop takeover lop_abc \
1028
1030
  ```
1029
1031
 
1030
1032
  The full public lifecycle (`open`, `get`, `list`, `pause`, `resume`, `close`,
1031
- `bind`, `verify`, `request_input`, `provide_input`, and the verbs above) is the
1033
+ `bind`, `verify`, `continue`, `request_input`, `provide_input`, and the verbs above) is the
1032
1034
  MCP `bclaw_loop(intent)` facade. Direct MCP `open` requires
1033
1035
  `allow_orphan=true`; review and ideation normally start through
1034
1036
  `bclaw_coordinate` so opening and dispatch stay one operation. See the
@@ -2032,7 +2034,7 @@ The default catalog is intentionally small and centred on the canonical grammar.
2032
2034
  |---|---|
2033
2035
  | `bclaw_coordinate(intent)` | Assign, consult, review, reroute, or summarize across agents. Pass `open_loop: true` on `intent="review"` to also dispatch the reviewer turn. |
2034
2036
  | `bclaw_dispatch(intent)` | Parallelize execute across a sequence's lanes (analysis / execute / review). |
2035
- | `bclaw_loop(intent)` | Open, inspect, or drive a multi-turn loop. The public lifecycle is `open`, `get`, `list`, `turn`, `complete_turn`, `advance`, `add_artifact`, `pause`, `resume`, and `close`; implementation loops also add engine-only `bind` (validate the linked sequence and enter `execute`, never spawn) and `verify`, and any kind may use `request_input` / `provide_input`. Trusted `turn(dispatch=true)` is the common worker launch path. `bclaw_coordinate` / `bclaw_dispatch` remain ergonomic shortcuts. A direct `open` must include `allow_orphan: true` to acknowledge that the caller will dispatch or drive it. |
2037
+ | `bclaw_loop(intent)` | Open, inspect, or drive a multi-turn loop. `continue` persists and applies policy-governed cross-loop progression through the public mutation path. Implementation loops add engine-only `bind` and `verify`; any kind may use `request_input` / `provide_input`. Trusted `turn(dispatch=true)` remains the only worker launch path. A direct `open` must include `allow_orphan: true`. |
2036
2038
 
2037
2039
  **Sequences**:
2038
2040
 
package/docs/code-map.md CHANGED
@@ -47,6 +47,8 @@ brainclaw code-map status
47
47
  ```
48
48
  Code Map status
49
49
  Store: present
50
+ Root: /workspace/apps/api
51
+ Path: /workspace/apps/api/.brainclaw/code
50
52
  Freshness: fresh
51
53
  Files: 142
52
54
  Nodes: 1873
@@ -212,6 +214,14 @@ Code Map to **that child** — the same per-project scoping that powers `bclaw_w
212
214
  juggling. A submodule that is itself an application (under e.g. `apps/`) is indexed
213
215
  like any other directory.
214
216
 
217
+ Both CLI and MCP status responses disclose the exact resolved project root and
218
+ Code Map store path. The MCP response additionally includes `active_source`, the
219
+ resolved project identity, the running server version, and the package version
220
+ visible on disk. In a monorepo, compare these fields before concluding that an
221
+ index is missing: a root store and a child store are intentionally distinct. If
222
+ the versions differ, restart the MCP server; if the roots differ, select the
223
+ intended project/session (or pass `cascade=true` at the workspace root).
224
+
215
225
  ### Cascading a multi-project workspace (`--cascade`)
216
226
 
217
227
  In a `project_mode: multi-project` workspace, one refresh at the root can index
@@ -363,6 +363,7 @@ Additional shared and engine-owned actions complete the lifecycle:
363
363
  - **request_input** / **provide_input** — bounded, evidence-backed operator clarification usable by any protocol.
364
364
  - **bind** — implementation-loop engine action that validates the linked sequence and advances to `execute`; it never launches a worker.
365
365
  - **verify** — implementation/debug engine action that runs the opener-configured command outside the loop lock, then records a verification-attested report.
366
+ - **continue** — orchestration action above the Loop engine. It evaluates an attested `next_action`, persists `AUTO | REQUIRE_APPROVAL | DENY`, and applies supported actions through the same public `open`/`bind` handlers.
366
367
 
367
368
  Artifact authority is sealed at these verb boundaries. `produced_by` is
368
369
  derived from the authenticated slot/engine/coordinator context. A narrative
@@ -396,6 +397,7 @@ type BclawLoopInput = BclawLoopCallerEnvelope & (
396
397
  | { intent: 'close'; loop_id: LoopId; status: 'completed' | 'cancelled' | 'blocked'; reason?: string; expected_version?: number }
397
398
  | { intent: 'verify'; loop_id: LoopId }
398
399
  | { intent: 'bind'; loop_id: LoopId; dry_run?: boolean; lanes?: string[]; auto_execute?: boolean; model?: string; max_assignments?: number }
400
+ | { intent: 'continue'; loop_id: LoopId; action_index?: number; autonomy_mode?: 'autonomous' | 'require_approval' | 'deny'; risk?: 'normal' | 'protected' }
399
401
  | { intent: 'request_input'; loop_id: LoopId; slot_id: SlotId; phase: string; question_text: string; evidence: string[]; suggested_default?: string; options?: OperatorQuestionOption[]; pause_scope: 'slot' | 'loop'; on_timeout: 'use_default' | 'cancel_loop' | 'continue_incomplete'; timeout_at?: string; expected_version?: number }
400
402
  | { intent: 'provide_input'; loop_id: LoopId; replies_to: string; resolved_via: 'answer' | 'choose' | 'skip' | 'timeout_default'; answer_text?: string; chosen_option_id?: string; by?: 'operator' | 'system'; expected_version?: number }
401
403
  | { intent: 'get'; loop_id: LoopId; include_events?: boolean }
@@ -470,6 +472,29 @@ The shared lifecycle verbs are `turn`, `complete_turn`, `advance`,
470
472
  use engine-only `bind` to validate their linked sequence and enter `execute`,
471
473
  then `turn(dispatch:true)` for worker slots; `verify` runs their declared command.
472
474
 
475
+ ### Persisted continuation authority
476
+
477
+ An accepted ideation synthesis and an attested implementation handoff no
478
+ longer expose ungoverned downstream mutations. Their `next_actions` point to
479
+ `bclaw_loop(intent="continue")`. The
480
+ continuation record binds the source loop, iteration, sealed artifact digest,
481
+ canonical action hash and policy version into a deterministic key. It is
482
+ written before the downstream mutation.
483
+
484
+ For Ideation→Implementation, `AUTO` invokes the ordinary public `open` handler
485
+ with that key in `linked.continuation_key`, then invokes engine-only `bind`.
486
+ For Implementation→Review, it deterministically selects a project-registered,
487
+ spawnable review-capable identity that did not occupy an implementation slot,
488
+ then invokes the ordinary public `bclaw_coordinate(intent="review",
489
+ open_loop=true)` path. If no independent reviewer exists, it fails closed.
490
+ A retry first scans existing loops for the key, so a crash after either public
491
+ mutation but before the response reuses the same loop. A live concurrent owner
492
+ is observed rather than stolen.
493
+ `REQUIRE_APPROVAL` creates an `ActionRequired` whose discriminated target is
494
+ the continuation; approval resumes the same record, while rejection or expiry
495
+ persists `DENY`. Unsupported actions, placeholders, missing evidence and
496
+ ambiguous downstreams fail closed.
497
+
473
498
  ### Clarification is a cross-cutting primitive
474
499
 
475
500
  Clarification is deliberately not a sixth protocol. Any workflow can call
@@ -493,6 +518,11 @@ The Loop engine is a **control plane**; existing primitives remain the **data pl
493
518
 
494
519
  A Loop never copies these objects — it links them. Deleting the linked primitive does not break the loop; the reference just becomes dangling, surfaced in diagnostics.
495
520
 
521
+ Inline `LoopArtifact.body` values are capped at **4096 UTF-8 bytes**, not 4096
522
+ characters. Larger task and result text remains available through its source
523
+ object or a `ref`; any inline projection is byte-truncated with an explicit
524
+ `…[truncated]` marker. This contract is identical for review and ideation.
525
+
496
526
  ## Per-protocol guides
497
527
 
498
528
  Each of the five kinds has its own operator-facing guide with the same
@@ -408,7 +408,12 @@ will still succeed. A follow-up PR will strip the dead handler code.
408
408
  changelog records the published MCP surface fingerprint. When a tool
409
409
  name, tier, category, or input schema changes, the test fails until
410
410
  this section is updated.
411
- - MCP public surface fingerprint: `sha256:681c47cba85b79c3`
411
+ - MCP public surface fingerprint: `sha256:be86e5571fcd0226`
412
+ (updated 2026-08-24 for persisted continuation authority: additive
413
+ `bclaw_loop(intent="continue")` inputs `action_index`, `autonomy_mode`, and
414
+ `risk`; the intent evaluates an attested Ideation→Implementation action,
415
+ persists AUTO/REQUIRE_APPROVAL/DENY, and reuses the public open/bind path.)
416
+ Previous: `sha256:681c47cba85b79c3`
412
417
  (`LoopSlotInput` gains optional `lane`, `scope_hint`, `plan_ids`, and
413
418
  `step_ids` fields so implementation-loop lane scope and provenance survive
414
419
  through the public facade. Existing callers remain valid.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainclaw",
3
- "version": "1.28.0",
3
+ "version": "1.28.2",
4
4
  "description": "Shared project memory for humans and coding agents.",
5
5
  "type": "module",
6
6
  "repository": {