brainclaw 1.26.2 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-coordination.js +65 -1
- package/dist/commands/attempt-authority.js +80 -0
- package/dist/commands/harvest.js +140 -61
- package/dist/commands/loop.js +34 -0
- package/dist/commands/loops-handlers.js +143 -15
- package/dist/commands/mcp-catalog.js +52 -18
- package/dist/commands/mcp-schemas.generated.js +64 -0
- package/dist/commands/mcp-write-claims.js +128 -1
- package/dist/commands/mcp-write-coordination.js +149 -76
- package/dist/core/agent-capability.js +1 -1
- package/dist/core/agentrun-reconciler.js +148 -22
- package/dist/core/agentruns.js +254 -29
- package/dist/core/assignment-request-schema.js +7 -0
- package/dist/core/assignment-sweeper.js +5 -3
- package/dist/core/assignments.js +131 -33
- package/dist/core/claim-request-schema.js +7 -0
- package/dist/core/claims.js +53 -2
- package/dist/core/dispatch-status.js +16 -6
- package/dist/core/dispatcher.js +51 -51
- package/dist/core/entity-operations.js +20 -0
- package/dist/core/events.js +4 -0
- package/dist/core/execution-adapters.js +189 -14
- package/dist/core/execution-contract.js +345 -0
- package/dist/core/execution.js +130 -16
- package/dist/core/facade-schema.js +3 -0
- package/dist/core/harness-adapters/base.js +150 -0
- package/dist/core/harness-adapters/claude.js +39 -0
- package/dist/core/harness-adapters/codex.js +57 -0
- package/dist/core/harness-adapters/harvest.js +109 -0
- package/dist/core/harness-adapters/index.js +8 -0
- package/dist/core/harness-adapters/prompt-only.js +13 -0
- package/dist/core/harness-adapters/registry.js +48 -0
- package/dist/core/harness-adapters/result.js +33 -0
- package/dist/core/harness-adapters/types.js +2 -0
- package/dist/core/ideation-loop-close.js +25 -2
- package/dist/core/instruction-templates.js +3 -2
- package/dist/core/loop-turn-dispatch.js +235 -0
- package/dist/core/loops/artifact-contract.js +11 -0
- package/dist/core/loops/attempt-authority.js +496 -0
- package/dist/core/loops/attempt-generations.js +509 -0
- package/dist/core/loops/attempt-reservation.js +197 -35
- package/dist/core/loops/attempt-rollout.js +404 -0
- package/dist/core/loops/attempt-takeover.js +155 -0
- package/dist/core/loops/bootstrap-acquire.js +7 -3
- package/dist/core/loops/brief-assembly.js +21 -4
- package/dist/core/loops/evidence.js +188 -0
- package/dist/core/loops/facade-schema.js +75 -11
- package/dist/core/loops/gate-policy.js +533 -0
- package/dist/core/loops/impl-bind.js +91 -81
- package/dist/core/loops/index.js +9 -0
- package/dist/core/loops/iteration-engine.js +31 -19
- package/dist/core/loops/kind-policies.js +90 -0
- package/dist/core/loops/lock.js +71 -13
- package/dist/core/loops/reconcile-turn.js +237 -18
- package/dist/core/loops/result-reducers.js +113 -10
- package/dist/core/loops/store.js +34 -3
- package/dist/core/loops/turn-execution.js +480 -0
- package/dist/core/loops/types.js +127 -3
- package/dist/core/loops/verbs.js +335 -99
- package/dist/core/loops/verify-command.js +105 -20
- package/dist/core/loops/workspace-digest.js +54 -0
- package/dist/core/review-loop-close.js +25 -3
- package/dist/core/review-loop-turn-dispatch.js +210 -161
- package/dist/core/runtime-signals.js +62 -25
- package/dist/core/schema.js +40 -0
- package/dist/core/spawn-check.js +3 -2
- package/dist/core/upgrades/backup.js +27 -4
- package/dist/facts.js +9 -8
- package/dist/facts.json +8 -7
- package/docs/cli.md +49 -1
- package/docs/concepts/attempt-authority.md +407 -0
- package/docs/concepts/evidence-attestations.md +135 -0
- package/docs/concepts/execution-contract.md +166 -0
- package/docs/concepts/harness-adapters.md +166 -0
- package/docs/concepts/ideation-loop.md +5 -4
- package/docs/concepts/loop-engine.md +302 -113
- package/docs/index.md +4 -1
- package/docs/integrations/codex.md +3 -3
- package/docs/integrations/mcp.md +59 -5
- package/docs/loops/debug.md +144 -0
- package/docs/loops/ideation.md +158 -0
- package/docs/loops/implementation.md +174 -0
- package/docs/loops/research.md +136 -0
- package/docs/loops/review.md +200 -0
- package/docs/mcp-schema-changelog.md +18 -5
- package/package.json +1 -1
|
@@ -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', `${
|
|
42
|
+
return path.join(runtimeDir(root), 'ack', `${key}.ack`);
|
|
42
43
|
}
|
|
43
|
-
return path.join(runtimeDir(root), '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
|
-
|
|
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
|
}
|
package/dist/core/schema.js
CHANGED
|
@@ -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(),
|
package/dist/core/spawn-check.js
CHANGED
|
@@ -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 {
|
|
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
|
-
??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2
|
+
// Source: brainclaw v1.28.0 on 2026-08-24T07:09:07.814Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.
|
|
5
|
-
"generated_at": "2026-08-
|
|
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-
|
|
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":
|
|
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":
|
|
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":
|
|
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.
|
|
3
|
-
"generated_at": "2026-08-
|
|
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-
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
|
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
|
|