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.
Files changed (88) hide show
  1. package/README.md +13 -0
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-coordination.js +65 -1
  4. package/dist/commands/attempt-authority.js +80 -0
  5. package/dist/commands/harvest.js +140 -61
  6. package/dist/commands/loop.js +34 -0
  7. package/dist/commands/loops-handlers.js +143 -15
  8. package/dist/commands/mcp-catalog.js +52 -18
  9. package/dist/commands/mcp-schemas.generated.js +64 -0
  10. package/dist/commands/mcp-write-claims.js +128 -1
  11. package/dist/commands/mcp-write-coordination.js +149 -76
  12. package/dist/core/agent-capability.js +1 -1
  13. package/dist/core/agentrun-reconciler.js +148 -22
  14. package/dist/core/agentruns.js +254 -29
  15. package/dist/core/assignment-request-schema.js +7 -0
  16. package/dist/core/assignment-sweeper.js +5 -3
  17. package/dist/core/assignments.js +131 -33
  18. package/dist/core/claim-request-schema.js +7 -0
  19. package/dist/core/claims.js +53 -2
  20. package/dist/core/dispatch-status.js +16 -6
  21. package/dist/core/dispatcher.js +51 -51
  22. package/dist/core/entity-operations.js +20 -0
  23. package/dist/core/events.js +4 -0
  24. package/dist/core/execution-adapters.js +189 -14
  25. package/dist/core/execution-contract.js +345 -0
  26. package/dist/core/execution.js +130 -16
  27. package/dist/core/facade-schema.js +3 -0
  28. package/dist/core/harness-adapters/base.js +150 -0
  29. package/dist/core/harness-adapters/claude.js +39 -0
  30. package/dist/core/harness-adapters/codex.js +57 -0
  31. package/dist/core/harness-adapters/harvest.js +109 -0
  32. package/dist/core/harness-adapters/index.js +8 -0
  33. package/dist/core/harness-adapters/prompt-only.js +13 -0
  34. package/dist/core/harness-adapters/registry.js +48 -0
  35. package/dist/core/harness-adapters/result.js +33 -0
  36. package/dist/core/harness-adapters/types.js +2 -0
  37. package/dist/core/ideation-loop-close.js +25 -2
  38. package/dist/core/instruction-templates.js +3 -2
  39. package/dist/core/loop-turn-dispatch.js +235 -0
  40. package/dist/core/loops/artifact-contract.js +11 -0
  41. package/dist/core/loops/attempt-authority.js +496 -0
  42. package/dist/core/loops/attempt-generations.js +509 -0
  43. package/dist/core/loops/attempt-reservation.js +197 -35
  44. package/dist/core/loops/attempt-rollout.js +404 -0
  45. package/dist/core/loops/attempt-takeover.js +155 -0
  46. package/dist/core/loops/bootstrap-acquire.js +7 -3
  47. package/dist/core/loops/brief-assembly.js +21 -4
  48. package/dist/core/loops/evidence.js +188 -0
  49. package/dist/core/loops/facade-schema.js +75 -11
  50. package/dist/core/loops/gate-policy.js +533 -0
  51. package/dist/core/loops/impl-bind.js +91 -81
  52. package/dist/core/loops/index.js +9 -0
  53. package/dist/core/loops/iteration-engine.js +31 -19
  54. package/dist/core/loops/kind-policies.js +90 -0
  55. package/dist/core/loops/lock.js +71 -13
  56. package/dist/core/loops/reconcile-turn.js +237 -18
  57. package/dist/core/loops/result-reducers.js +113 -10
  58. package/dist/core/loops/store.js +34 -3
  59. package/dist/core/loops/turn-execution.js +480 -0
  60. package/dist/core/loops/types.js +127 -3
  61. package/dist/core/loops/verbs.js +335 -99
  62. package/dist/core/loops/verify-command.js +105 -20
  63. package/dist/core/loops/workspace-digest.js +54 -0
  64. package/dist/core/review-loop-close.js +25 -3
  65. package/dist/core/review-loop-turn-dispatch.js +210 -161
  66. package/dist/core/runtime-signals.js +62 -25
  67. package/dist/core/schema.js +40 -0
  68. package/dist/core/spawn-check.js +3 -2
  69. package/dist/core/upgrades/backup.js +27 -4
  70. package/dist/facts.js +9 -8
  71. package/dist/facts.json +8 -7
  72. package/docs/cli.md +49 -1
  73. package/docs/concepts/attempt-authority.md +407 -0
  74. package/docs/concepts/evidence-attestations.md +135 -0
  75. package/docs/concepts/execution-contract.md +166 -0
  76. package/docs/concepts/harness-adapters.md +166 -0
  77. package/docs/concepts/ideation-loop.md +5 -4
  78. package/docs/concepts/loop-engine.md +302 -113
  79. package/docs/index.md +4 -1
  80. package/docs/integrations/codex.md +3 -3
  81. package/docs/integrations/mcp.md +59 -5
  82. package/docs/loops/debug.md +144 -0
  83. package/docs/loops/ideation.md +158 -0
  84. package/docs/loops/implementation.md +174 -0
  85. package/docs/loops/research.md +136 -0
  86. package/docs/loops/review.md +200 -0
  87. package/docs/mcp-schema-changelog.md +18 -5
  88. package/package.json +1 -1
@@ -36,15 +36,17 @@ function runtimeDir(root) {
36
36
  * `runtime/ack/<id>.ack` location (pln#476); the liveness signals live under
37
37
  * `runtime/signal/<id>.<signal>`.
38
38
  */
39
- export function getRuntimeSignalPath(root, assignmentId, signal) {
39
+ export function getRuntimeSignalPath(root, assignmentId, signal, runId) {
40
+ const key = runId ? `${assignmentId}.${runId}` : assignmentId;
40
41
  if (signal === 'ack') {
41
- return path.join(runtimeDir(root), 'ack', `${assignmentId}.ack`);
42
+ return path.join(runtimeDir(root), 'ack', `${key}.ack`);
42
43
  }
43
- return path.join(runtimeDir(root), 'signal', `${assignmentId}.${signal}`);
44
+ return path.join(runtimeDir(root), 'signal', `${key}.${signal}`);
44
45
  }
45
46
  /** Absolute path for a captured stream log (`runtime/log/<id>.{stdout,stderr}.log`). */
46
- export function getRuntimeLogPath(root, assignmentId, stream) {
47
- return path.join(runtimeDir(root), 'log', `${assignmentId}.${stream}.log`);
47
+ export function getRuntimeLogPath(root, assignmentId, stream, runId) {
48
+ const key = runId ? `${assignmentId}.${runId}` : assignmentId;
49
+ return path.join(runtimeDir(root), 'log', `${key}.${stream}.log`);
48
50
  }
49
51
  /**
50
52
  * Worktree-local heartbeat path (sprint 1.5). The project-root signal path is
@@ -55,8 +57,8 @@ export function getRuntimeLogPath(root, assignmentId, stream) {
55
57
  * write, so briefs point step-0 here, and every heartbeat reader checks BOTH
56
58
  * locations.
57
59
  */
58
- export function getWorktreeHeartbeatPath(worktreePath, assignmentId) {
59
- return path.join(worktreePath, `.brainclaw-heartbeat-${assignmentId}`);
60
+ export function getWorktreeHeartbeatPath(worktreePath, assignmentId, runId) {
61
+ return path.join(worktreePath, `.brainclaw-heartbeat-${assignmentId}${runId ? `-${runId}` : ''}`);
60
62
  }
61
63
  /** Ensure the ack / signal / log directories exist (best-effort, recursive). */
62
64
  export function ensureRuntimeDirs(root) {
@@ -65,14 +67,43 @@ export function ensureRuntimeDirs(root) {
65
67
  fs.mkdirSync(path.join(base, sub), { recursive: true });
66
68
  }
67
69
  }
68
- export function signalExists(root, assignmentId, signal) {
70
+ export function signalExists(root, assignmentId, signal, runId) {
69
71
  try {
70
- return fs.existsSync(getRuntimeSignalPath(root, assignmentId, signal));
72
+ return fs.existsSync(getRuntimeSignalPath(root, assignmentId, signal, runId));
71
73
  }
72
74
  catch {
73
75
  return false;
74
76
  }
75
77
  }
78
+ /** Read the bootstrap's effective-environment attestation. Empty legacy acks return undefined. */
79
+ export function readContractAck(root, assignmentId, runId) {
80
+ try {
81
+ const raw = fs.readFileSync(getRuntimeSignalPath(root, assignmentId, 'ack', runId), 'utf8').trim();
82
+ if (!raw)
83
+ return undefined;
84
+ const parsed = JSON.parse(raw);
85
+ if ((parsed.status === 'accepted' || parsed.status === 'rejected')
86
+ && typeof parsed.turn_id === 'string'
87
+ && typeof parsed.run_id === 'string'
88
+ && typeof parsed.nonce === 'string'
89
+ && typeof parsed.contract_hash === 'string'
90
+ && typeof parsed.capability_snapshot_hash === 'string') {
91
+ return {
92
+ status: parsed.status,
93
+ turn_id: parsed.turn_id,
94
+ run_id: parsed.run_id,
95
+ nonce: parsed.nonce,
96
+ contract_hash: parsed.contract_hash,
97
+ capability_snapshot_hash: parsed.capability_snapshot_hash,
98
+ ...(typeof parsed.attempt_epoch === 'number' ? { attempt_epoch: parsed.attempt_epoch } : {}),
99
+ ...(typeof parsed.workspace_digest === 'string' ? { workspace_digest: parsed.workspace_digest } : {}),
100
+ ...(typeof parsed.cwd === 'string' ? { cwd: parsed.cwd } : {}),
101
+ };
102
+ }
103
+ }
104
+ catch { /* absent, legacy or malformed */ }
105
+ return undefined;
106
+ }
76
107
  function readHeartbeatFile(p) {
77
108
  try {
78
109
  const stat = fs.statSync(p);
@@ -103,10 +134,10 @@ function readHeartbeatFile(p) {
103
134
  * worktree-local heartbeat — sandboxed workers can only write the latter. When
104
135
  * both exist, the freshest mtime wins.
105
136
  */
106
- export function readHeartbeat(root, assignmentId, worktreePath) {
107
- const projectInfo = readHeartbeatFile(getRuntimeSignalPath(root, assignmentId, 'heartbeat'));
137
+ export function readHeartbeat(root, assignmentId, worktreePath, runId) {
138
+ const projectInfo = readHeartbeatFile(getRuntimeSignalPath(root, assignmentId, 'heartbeat', runId));
108
139
  const worktreeInfo = worktreePath
109
- ? readHeartbeatFile(getWorktreeHeartbeatPath(worktreePath, assignmentId))
140
+ ? readHeartbeatFile(getWorktreeHeartbeatPath(worktreePath, assignmentId, runId))
110
141
  : { exists: false };
111
142
  if (!projectInfo.exists)
112
143
  return worktreeInfo;
@@ -120,17 +151,17 @@ export function readHeartbeat(root, assignmentId, worktreePath) {
120
151
  * still produces a legacy presence-only marker, which stays a valid life-sign
121
152
  * via signalExists but is NOT accepted as turn-owned evidence (PR2b-c).
122
153
  */
123
- export function writeCompletionSignal(root, assignmentId, body) {
124
- const p = getRuntimeSignalPath(root, assignmentId, body.status);
154
+ export function writeCompletionSignal(root, assignmentId, body, runId) {
155
+ const p = getRuntimeSignalPath(root, assignmentId, body.status, runId);
125
156
  fs.mkdirSync(path.dirname(p), { recursive: true });
126
157
  fs.writeFileSync(p, JSON.stringify(body), 'utf-8');
127
158
  }
128
159
  /** Parse ONE turn-keyed sentinel body, or undefined if absent / legacy
129
160
  * presence-only / non-JSON / missing correlation keys. Never throws. */
130
- function readOneCompletionSignal(root, assignmentId, status) {
161
+ function readOneCompletionSignal(root, assignmentId, status, runId) {
131
162
  let raw;
132
163
  try {
133
- raw = fs.readFileSync(getRuntimeSignalPath(root, assignmentId, status), 'utf-8').trim();
164
+ raw = fs.readFileSync(getRuntimeSignalPath(root, assignmentId, status, runId), 'utf-8').trim();
134
165
  }
135
166
  catch {
136
167
  return undefined; // sentinel absent
@@ -147,6 +178,12 @@ function readOneCompletionSignal(root, assignmentId, status) {
147
178
  turn_id: parsed.turn_id,
148
179
  run_id: parsed.run_id,
149
180
  nonce: parsed.nonce,
181
+ ...(typeof parsed.contract_hash === 'string' ? { contract_hash: parsed.contract_hash } : {}),
182
+ ...(typeof parsed.capability_snapshot_hash === 'string'
183
+ ? { capability_snapshot_hash: parsed.capability_snapshot_hash }
184
+ : {}),
185
+ ...(typeof parsed.attempt_epoch === 'number' ? { attempt_epoch: parsed.attempt_epoch } : {}),
186
+ ...(typeof parsed.workspace_digest === 'string' ? { workspace_digest: parsed.workspace_digest } : {}),
150
187
  status: parsed.status,
151
188
  at: typeof parsed.at === 'string' ? parsed.at : '',
152
189
  };
@@ -162,10 +199,10 @@ function readOneCompletionSignal(root, assignmentId, status) {
162
199
  * and WITHHOLD an irreversible auto-stop (spec §13 R4), rather than silently
163
200
  * collapsing to one. Legacy presence-only markers read as absent here.
164
201
  */
165
- export function readCompletionSignals(root, assignmentId) {
202
+ export function readCompletionSignals(root, assignmentId, runId) {
166
203
  const out = {};
167
- const completed = readOneCompletionSignal(root, assignmentId, 'completed');
168
- const failed = readOneCompletionSignal(root, assignmentId, 'failed');
204
+ const completed = readOneCompletionSignal(root, assignmentId, 'completed', runId);
205
+ const failed = readOneCompletionSignal(root, assignmentId, 'failed', runId);
169
206
  if (completed)
170
207
  out.completed = completed;
171
208
  if (failed)
@@ -178,8 +215,8 @@ export function readCompletionSignals(root, assignmentId) {
178
215
  * CALLERS THAT ACT IRREVERSIBLY must use {@link readCompletionSignals} instead
179
216
  * so a completed+failed contradiction is not hidden (spec §13 R4).
180
217
  */
181
- export function readCompletionSignal(root, assignmentId) {
182
- const both = readCompletionSignals(root, assignmentId);
218
+ export function readCompletionSignal(root, assignmentId, runId) {
219
+ const both = readCompletionSignals(root, assignmentId, runId);
183
220
  return both.completed ?? both.failed;
184
221
  }
185
222
  /**
@@ -212,9 +249,9 @@ export function decodeOemAwareBuffer(buf) {
212
249
  return out;
213
250
  }
214
251
  /** Read the tail of a captured stream log (for failed_silent diagnostics). */
215
- export function readLogTail(root, assignmentId, stream, maxBytes = 2000) {
252
+ export function readLogTail(root, assignmentId, stream, maxBytes = 2000, runId) {
216
253
  try {
217
- const p = getRuntimeLogPath(root, assignmentId, stream);
254
+ const p = getRuntimeLogPath(root, assignmentId, stream, runId);
218
255
  const buf = fs.readFileSync(p);
219
256
  let slice = buf.length > maxBytes ? buf.subarray(buf.length - maxBytes) : buf;
220
257
  // A byte-offset tail can start mid-UTF-8-sequence; dropping leading
@@ -285,7 +322,7 @@ export function latestWorktreeFileMtimeMs(worktreePath, maxDepth = 4) {
285
322
  * fixing the false-`stalled` verdict (field debrief P1#1). Returns undefined
286
323
  * when nothing is observable.
287
324
  */
288
- export function latestActivityMs(root, assignmentId, worktreePath) {
325
+ export function latestActivityMs(root, assignmentId, worktreePath, runId) {
289
326
  let latest;
290
327
  const bump = (ms) => {
291
328
  if (ms !== undefined && (latest === undefined || ms > latest))
@@ -293,7 +330,7 @@ export function latestActivityMs(root, assignmentId, worktreePath) {
293
330
  };
294
331
  for (const stream of ['stdout', 'stderr']) {
295
332
  try {
296
- bump(fs.statSync(getRuntimeLogPath(root, assignmentId, stream)).mtimeMs);
333
+ bump(fs.statSync(getRuntimeLogPath(root, assignmentId, stream, runId)).mtimeMs);
297
334
  }
298
335
  catch { /* no log */ }
299
336
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { CapabilitySnapshotSchema, ExecutionContractRefSchema, RuntimeCapabilityObservationSchema } from './execution-contract.js';
2
3
  // --- Helpers ---
3
4
  /** Coerce legacy effort strings ("30min", "2h", "1d") to integer minutes for migration.
4
5
  * Already-numeric values pass through unchanged. Unparseable strings → undefined. */
@@ -797,6 +798,9 @@ export const AssignmentSchema = z.object({
797
798
  description: z.string(),
798
799
  lane: z.string().optional(),
799
800
  worktree_path: z.string().optional(),
801
+ /** Immutable attempt contract identity; optional for legacy records. */
802
+ execution_contract_ref: ExecutionContractRefSchema.optional(),
803
+ capability_snapshot: CapabilitySnapshotSchema.optional(),
800
804
  // Status FSM
801
805
  status: AssignmentStatusSchema,
802
806
  status_reason: z.string().optional(),
@@ -873,6 +877,26 @@ export const AgentRunSchema = z.object({
873
877
  shell: z.string().optional(),
874
878
  pid: z.number().int().positive().optional(),
875
879
  provider_run_id: z.string().optional(),
880
+ /** Immutable attempt contract identity; optional for legacy records. */
881
+ execution_contract_ref: ExecutionContractRefSchema.optional(),
882
+ capability_snapshot: CapabilitySnapshotSchema.optional(),
883
+ /** Runtime observation is additive and never mutates the hashed capability snapshot. */
884
+ runtime_capability_observation: RuntimeCapabilityObservationSchema.optional(),
885
+ harness_exit_diagnostic: z.object({
886
+ adapter_id: z.string().min(1),
887
+ adapter_version: z.string().min(1),
888
+ transport_status: z.enum(['completed', 'failed', 'timed_out', 'cancelled']),
889
+ protocol_status: z.enum(['valid', 'invalid', 'partial', 'absent']),
890
+ message: z.string().min(1).optional(),
891
+ }).optional(),
892
+ /** Monotone fence: once present, no reconciler may auto-converge or respawn this generation. */
893
+ execution_contract_anomaly: z.object({
894
+ detected_at: z.string().min(1),
895
+ source: z.enum(['bootstrap_ack', 'completion_signal', 'lane_result', 'reconciler']),
896
+ reason: z.string().min(1),
897
+ accepted_contract_hash: z.string().optional(),
898
+ accepted_capability_snapshot_hash: z.string().optional(),
899
+ }).optional(),
876
900
  created_at: z.string(),
877
901
  updated_at: z.string().optional(),
878
902
  launched_at: z.string().optional(),
@@ -1012,6 +1036,9 @@ export const RuntimeEventTypeSchema = z.enum([
1012
1036
  // pln#521 P4 — a turn-owned loop artifact was harvested + integrated into the loop
1013
1037
  // by reconcileTurn (observability for the harvest path).
1014
1038
  'loop_artifact_harvested',
1039
+ // AttemptAuthority v2 — causal takeover telemetry. The immutable close cell
1040
+ // remains authoritative; this event is an operator-facing projection only.
1041
+ 'attempt_takeover',
1015
1042
  ]);
1016
1043
  /**
1017
1044
  * pln#526 — LANE-RESULT convention. A dispatched worker writes a single
@@ -1038,6 +1065,12 @@ export const LaneResultSchema = z.object({
1038
1065
  turn_id: z.string().optional(),
1039
1066
  run_id: z.string().optional(),
1040
1067
  nonce: z.string().optional(),
1068
+ /** AttemptAuthority v2 full-fence coordinates (required by v2 acceptance). */
1069
+ attempt_epoch: z.number().int().nonnegative().optional(),
1070
+ workspace_digest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
1071
+ /** ExecutionContract v1 acceptance echoed by the worker/bootstrap. */
1072
+ execution_contract_hash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
1073
+ capability_snapshot_hash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
1041
1074
  status: z.enum(['completed', 'blocked', 'failed']),
1042
1075
  summary: z.string(),
1043
1076
  /** Paths or refs the worker produced (commits, files, docs). */
@@ -1058,6 +1091,11 @@ export const LaneResultSchema = z.object({
1058
1091
  * reconcile this to its phase's required artifact type.
1059
1092
  */
1060
1093
  artifact_type: z.string().min(1).optional(),
1094
+ /** Synthesis-only executable acceptance policy for the downstream implementation loop. */
1095
+ implementation_verify: z.object({
1096
+ command: z.array(z.string().min(1)).min(1),
1097
+ timeout_ms: z.number().int().positive().max(15 * 60 * 1000).optional(),
1098
+ }).optional(),
1061
1099
  /**
1062
1100
  * pln#628 Focus 4B — review-loop verdict. A worker running a review-loop turn
1063
1101
  * sets this to signal whether the change is good to merge (`approve`) or needs
@@ -1087,6 +1125,8 @@ export const RuntimeEventSchema = z.object({
1087
1125
  // `run_id` already present above; `nonce` == launch-generation token.
1088
1126
  turn_id: z.string().optional(),
1089
1127
  nonce: z.string().optional(),
1128
+ attempt_epoch: z.number().int().nonnegative().optional(),
1129
+ workspace_digest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
1090
1130
  claim_id: z.string().optional(),
1091
1131
  message_id: z.string().optional(),
1092
1132
  plan_id: z.string().optional(),
@@ -18,7 +18,8 @@ import fs from 'node:fs';
18
18
  import os from 'node:os';
19
19
  import path from 'node:path';
20
20
  import { spawnSync } from 'node:child_process';
21
- import { buildInvokeCommand, getSpawnableAgents, getCapabilityProfile, } from './agent-capability.js';
21
+ import { getSpawnableAgents, getCapabilityProfile, } from './agent-capability.js';
22
+ import { buildHarnessInvocation } from './harness-adapters/index.js';
22
23
  import { defaultExecutionAdapter, resolveBinaryOnPath } from './execution-adapters.js';
23
24
  import { signalExists, readLogTail } from './runtime-signals.js';
24
25
  import { recognizeStderrSignature } from './dispatch-status.js';
@@ -63,7 +64,7 @@ export async function checkAgentSpawn(agent, options = {}) {
63
64
  return { agent, binary: profile.invoke_binary, status: 'not_installed', delivered: false, completed: false, duration_ms: 0, detail: `binary '${profile.invoke_binary}' not on PATH` };
64
65
  }
65
66
  const invoke = options.probeFor?.(agent)
66
- ?? buildInvokeCommand(agent, options.probePrompt ?? DEFAULT_PROBE_PROMPT, { mode: 'consult' });
67
+ ?? buildHarnessInvocation(agent, options.probePrompt ?? DEFAULT_PROBE_PROMPT, { mode: 'consult' })?.invoke;
67
68
  if (!invoke) {
68
69
  return { agent, binary, status: 'no_template', delivered: false, completed: false, duration_ms: 0, detail: 'could not build invoke command' };
69
70
  }
@@ -6,6 +6,29 @@ export const BACKUP_DIR_PREFIX = '.brainclaw.bak-';
6
6
  export const BACKUP_MANIFEST_FILENAME = 'backup.json';
7
7
  export const ROLLBACK_PARKED_PREFIX = '.brainclaw.rollback-';
8
8
  export const ROLLBACK_STAGING_PREFIX = '.brainclaw.restoring-';
9
+ const RETRYABLE_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
10
+ const RENAME_ATTEMPTS = 6;
11
+ const RENAME_RETRY_DELAY_MS = 25;
12
+ function sleepSync(ms) {
13
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
14
+ }
15
+ /** NTFS/Defender can transiently deny an otherwise valid directory rename. */
16
+ function renameWithRetry(from, to) {
17
+ for (let attempt = 0; attempt < RENAME_ATTEMPTS; attempt += 1) {
18
+ try {
19
+ fs.renameSync(from, to);
20
+ return;
21
+ }
22
+ catch (error) {
23
+ const code = error instanceof Error && 'code' in error
24
+ ? error.code
25
+ : undefined;
26
+ if (!code || !RETRYABLE_RENAME_CODES.has(code) || attempt === RENAME_ATTEMPTS - 1)
27
+ throw error;
28
+ sleepSync(RENAME_RETRY_DELAY_MS * (attempt + 1));
29
+ }
30
+ }
31
+ }
9
32
  export const BackupManifestSchema = z.object({
10
33
  schema_version: z.literal(1),
11
34
  created_at: z.string().datetime(),
@@ -74,7 +97,7 @@ export function createBackup(options) {
74
97
  };
75
98
  fs.writeFileSync(path.join(stagingPath, BACKUP_MANIFEST_FILENAME), JSON.stringify(manifest, null, 2), 'utf-8');
76
99
  try {
77
- fs.renameSync(stagingPath, finalPath);
100
+ renameWithRetry(stagingPath, finalPath);
78
101
  }
79
102
  catch (error) {
80
103
  try {
@@ -198,7 +221,7 @@ export function restoreBackup(options) {
198
221
  let parked = false;
199
222
  if (fs.existsSync(storePath)) {
200
223
  try {
201
- fs.renameSync(storePath, parkedPath);
224
+ renameWithRetry(storePath, parkedPath);
202
225
  parked = true;
203
226
  }
204
227
  catch (error) {
@@ -209,7 +232,7 @@ export function restoreBackup(options) {
209
232
  // Step 4: swap staging → live. On failure, un-park so the store
210
233
  // is never left missing; staging dir is cleaned.
211
234
  try {
212
- fs.renameSync(stagingPath, storePath);
235
+ renameWithRetry(stagingPath, storePath);
213
236
  }
214
237
  catch (error) {
215
238
  const swapMessage = error.message;
@@ -218,7 +241,7 @@ export function restoreBackup(options) {
218
241
  throw new BackupError('restore_swap_failed', `Could not swap staging into live path: ${swapMessage}`);
219
242
  }
220
243
  try {
221
- fs.renameSync(parkedPath, storePath);
244
+ renameWithRetry(parkedPath, storePath);
222
245
  }
223
246
  catch (unparkError) {
224
247
  throw new BackupError('restore_catastrophic', `Could not swap staging into live path: ${swapMessage}; also failed to restore parked live store: ${unparkError.message}. ` +
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.26.2 on 2026-08-22T12:18:05.969Z
2
+ // Source: brainclaw v1.28.0 on 2026-08-24T07:09:07.814Z
3
3
  export const FACTS = {
4
- "version": "1.26.2",
5
- "generated_at": "2026-08-22T12:18:05.969Z",
4
+ "version": "1.28.0",
5
+ "generated_at": "2026-08-24T07:09:07.814Z",
6
6
  "tools": {
7
7
  "count": 70,
8
8
  "published_count": 68,
@@ -231,7 +231,8 @@ export const FACTS = {
231
231
  "mcp_config_scope": "machine",
232
232
  "role_capabilities": [
233
233
  "execute",
234
- "review"
234
+ "review",
235
+ "consult"
235
236
  ],
236
237
  "max_concurrent_tasks": 5
237
238
  },
@@ -477,7 +478,7 @@ export const FACTS = {
477
478
  },
478
479
  "bench": {
479
480
  "schema": "brainclaw.bench.v1",
480
- "generated_at": "2026-08-22T12:18:03.871Z",
481
+ "generated_at": "2026-08-24T07:09:05.728Z",
481
482
  "node_version": "v24.19.0",
482
483
  "platform": "linux-x64",
483
484
  "repeats": 3,
@@ -486,7 +487,7 @@ export const FACTS = {
486
487
  "name": "cold_onboard",
487
488
  "volume": "empty",
488
489
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
489
- "duration_ms_median": 76,
490
+ "duration_ms_median": 81,
490
491
  "payload_chars_median": 1640,
491
492
  "payload_tokens_est_median": 410
492
493
  },
@@ -494,7 +495,7 @@ export const FACTS = {
494
495
  "name": "warm_work",
495
496
  "volume": "medium",
496
497
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
497
- "duration_ms_median": 123,
498
+ "duration_ms_median": 120,
498
499
  "payload_chars_median": 2626,
499
500
  "payload_tokens_est_median": 657
500
501
  },
@@ -502,7 +503,7 @@ export const FACTS = {
502
503
  "name": "first_edit",
503
504
  "volume": "medium",
504
505
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
505
- "duration_ms_median": 11,
506
+ "duration_ms_median": 13,
506
507
  "payload_chars_median": 1305,
507
508
  "payload_tokens_est_median": 326
508
509
  }
package/dist/facts.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.26.2",
3
- "generated_at": "2026-08-22T12:18:05.969Z",
2
+ "version": "1.28.0",
3
+ "generated_at": "2026-08-24T07:09:07.814Z",
4
4
  "tools": {
5
5
  "count": 70,
6
6
  "published_count": 68,
@@ -229,7 +229,8 @@
229
229
  "mcp_config_scope": "machine",
230
230
  "role_capabilities": [
231
231
  "execute",
232
- "review"
232
+ "review",
233
+ "consult"
233
234
  ],
234
235
  "max_concurrent_tasks": 5
235
236
  },
@@ -475,7 +476,7 @@
475
476
  },
476
477
  "bench": {
477
478
  "schema": "brainclaw.bench.v1",
478
- "generated_at": "2026-08-22T12:18:03.871Z",
479
+ "generated_at": "2026-08-24T07:09:05.728Z",
479
480
  "node_version": "v24.19.0",
480
481
  "platform": "linux-x64",
481
482
  "repeats": 3,
@@ -484,7 +485,7 @@
484
485
  "name": "cold_onboard",
485
486
  "volume": "empty",
486
487
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
487
- "duration_ms_median": 76,
488
+ "duration_ms_median": 81,
488
489
  "payload_chars_median": 1640,
489
490
  "payload_tokens_est_median": 410
490
491
  },
@@ -492,7 +493,7 @@
492
493
  "name": "warm_work",
493
494
  "volume": "medium",
494
495
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
495
- "duration_ms_median": 123,
496
+ "duration_ms_median": 120,
496
497
  "payload_chars_median": 2626,
497
498
  "payload_tokens_est_median": 657
498
499
  },
@@ -500,7 +501,7 @@
500
501
  "name": "first_edit",
501
502
  "volume": "medium",
502
503
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
503
- "duration_ms_median": 11,
504
+ "duration_ms_median": 13,
504
505
  "payload_chars_median": 1305,
505
506
  "payload_tokens_est_median": 326
506
507
  }
package/docs/cli.md CHANGED
@@ -1003,6 +1003,54 @@ brainclaw run claude-code # run the claude-code profile
1003
1003
  brainclaw run claude-code --dry # preview the resolved command
1004
1004
  ```
1005
1005
 
1006
+ ### `brainclaw loop <subcommand>`
1007
+
1008
+ Operator wrappers for the state-changing Loop verbs most useful from a shell.
1009
+ They drive the same shared runtime for review, ideation, implementation,
1010
+ research, and debug; they are not a review-only command group.
1011
+
1012
+ | Subcommand | Required arguments/options | Purpose |
1013
+ |---|---|---|
1014
+ | `turn <loop_id>` | `--slot <slot_id>` | Record a turn assignment. The CLI wrapper is state-only; MCP `turn` additionally supports trusted `dispatch:true`. |
1015
+ | `complete-turn <loop_id>` | `--slot <slot_id> --outcome <done\|failed\|cancelled>` | Complete a manually driven slot turn; optional `--artifact <json>`. AttemptAuthority v2 workers must also pass the full fence: `--assignment-id`, `--turn-id`, `--run-id`, `--nonce`, `--attempt-epoch`, `--execution-contract-hash`, and `--workspace-digest`. |
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
+ | `advance <loop_id>` | — | Advance through the protocol; optional `--to-phase`, `--force`, `--reason`. |
1018
+ | `add-artifact <loop_id>` | `--phase --type --body` | Attach a typed artifact; optional producer and ref. |
1019
+
1020
+ ```bash
1021
+ brainclaw loop advance lop_abc --json
1022
+ brainclaw loop takeover lop_abc \
1023
+ --slot lsl_abc --turn-id tat_abc --expected-epoch 0 \
1024
+ --cause "worker is no longer live" \
1025
+ --liveness-evidence "wrapper exited; heartbeat stale" \
1026
+ --external-effect-policy idempotent \
1027
+ --next-workspace-path ../brainclaw-retry --agent coordinator
1028
+ ```
1029
+
1030
+ The full public lifecycle (`open`, `get`, `list`, `pause`, `resume`, `close`,
1031
+ `bind`, `verify`, `request_input`, `provide_input`, and the verbs above) is the
1032
+ MCP `bclaw_loop(intent)` facade. Direct MCP `open` requires
1033
+ `allow_orphan=true`; review and ideation normally start through
1034
+ `bclaw_coordinate` so opening and dispatch stay one operation. See the
1035
+ [Loop Engine](concepts/loop-engine.md) and its five [protocol guides](loops/).
1036
+
1037
+ ### `brainclaw attempt-authority <subcommand>`
1038
+
1039
+ Two-release activation surface for AttemptAuthority v2 writers:
1040
+
1041
+ | Subcommand | Purpose |
1042
+ |---|---|
1043
+ | `status [--json]` | Inspect writer version, local authority home and active membership. |
1044
+ | `prepare --writers <agent_ids...>` | Publish a Release-A membership guard; optional epoch and audited actor. |
1045
+ | `ack --membership-epoch <n> --agent-id <id>` | Publish the named writer's signed immutable ACK. Writers may ACK in parallel. |
1046
+ | `activate --membership-epoch <n>` | Activate a fully acknowledged membership epoch. |
1047
+
1048
+ Do not use `activate` as the first rollout step. Drain old writers, create and
1049
+ verify a private store snapshot, complete every ACK, then canary Release B on
1050
+ the authority home. After the first v2 generation cell, direct downgrade is
1051
+ refused; use the controlled export/restore procedure in
1052
+ [Attempt authority](concepts/attempt-authority.md#migration-and-rollout-runbook).
1053
+
1006
1054
  ### `brainclaw plan create <text>`
1007
1055
 
1008
1056
  Create a shared work item.
@@ -1984,7 +2032,7 @@ The default catalog is intentionally small and centred on the canonical grammar.
1984
2032
  |---|---|
1985
2033
  | `bclaw_coordinate(intent)` | Assign, consult, review, reroute, or summarize across agents. Pass `open_loop: true` on `intent="review"` to also dispatch the reviewer turn. |
1986
2034
  | `bclaw_dispatch(intent)` | Parallelize execute across a sequence's lanes (analysis / execute / review). |
1987
- | `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 `bind` and `verify`, and any kind may use `request_input` / `provide_input`. `bclaw_coordinate` / `bclaw_dispatch` remain the ergonomic review and ideation shortcuts. A direct `open` must include `allow_orphan: true` to acknowledge that the caller will dispatch or drive it. |
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. |
1988
2036
 
1989
2037
  **Sequences**:
1990
2038