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
package/dist/core/execution.js
CHANGED
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import { resolveConcurrencyLimit, resolveResourceKey } from './agent-capability.js';
|
|
12
|
-
import { getRuntimeSignalPath } from './runtime-signals.js';
|
|
12
|
+
import { getRuntimeSignalPath, readContractAck } from './runtime-signals.js';
|
|
13
13
|
import { appendAuditEntry } from './audit.js';
|
|
14
14
|
import { loadAllSessions } from './identity.js';
|
|
15
15
|
import { loadConfig } from './config.js';
|
|
16
16
|
import { loadAssignment } from './assignments.js';
|
|
17
|
+
import { recordExecutionContractAnomaly } from './agentruns.js';
|
|
17
18
|
import { defaultExecutionAdapter, } from './execution-adapters.js';
|
|
18
19
|
function sleep(ms) {
|
|
19
20
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -29,26 +30,54 @@ function sleep(ms) {
|
|
|
29
30
|
* bclaw_assignment_update; the ack file lets us recognize a healthy
|
|
30
31
|
* spawn anyway).
|
|
31
32
|
*/
|
|
32
|
-
export function getAssignmentAckPath(cwd, assignmentId) {
|
|
33
|
-
return getRuntimeSignalPath(cwd, assignmentId, 'ack');
|
|
33
|
+
export function getAssignmentAckPath(cwd, assignmentId, runId) {
|
|
34
|
+
return getRuntimeSignalPath(cwd, assignmentId, 'ack', runId);
|
|
34
35
|
}
|
|
35
|
-
function isAssignmentAcked(assignmentId, cwd) {
|
|
36
|
+
function isAssignmentAcked(assignmentId, cwd, runId) {
|
|
36
37
|
// Fast path: the brief-ack sentinel was written by the worker shell.
|
|
37
|
-
if (fs.existsSync(getAssignmentAckPath(cwd, assignmentId)))
|
|
38
|
+
if (fs.existsSync(getAssignmentAckPath(cwd, assignmentId, runId)))
|
|
38
39
|
return true;
|
|
40
|
+
// A v2 generation must acknowledge its own run-scoped bootstrap. The stable
|
|
41
|
+
// Assignment may already be running/completed because of a prior epoch.
|
|
42
|
+
if (runId)
|
|
43
|
+
return false;
|
|
39
44
|
// Standard path: the worker called bclaw_assignment_update via MCP and
|
|
40
45
|
// moved the assignment past the offered/created state.
|
|
41
46
|
const assignment = loadAssignment(assignmentId, cwd);
|
|
42
47
|
return !!assignment && assignment.status !== 'created' && assignment.status !== 'offered';
|
|
43
48
|
}
|
|
44
|
-
async function waitForAssignmentHandshake(assignmentId, cwd, timeoutMs) {
|
|
49
|
+
async function waitForAssignmentHandshake(assignmentId, cwd, timeoutMs, runId) {
|
|
45
50
|
const deadline = Date.now() + timeoutMs;
|
|
46
51
|
while (Date.now() < deadline) {
|
|
47
|
-
if (isAssignmentAcked(assignmentId, cwd))
|
|
52
|
+
if (isAssignmentAcked(assignmentId, cwd, runId))
|
|
48
53
|
return true;
|
|
49
54
|
await sleep(100);
|
|
50
55
|
}
|
|
51
|
-
return isAssignmentAcked(assignmentId, cwd);
|
|
56
|
+
return isAssignmentAcked(assignmentId, cwd, runId);
|
|
57
|
+
}
|
|
58
|
+
function normalizeWorkspacePath(value) {
|
|
59
|
+
try {
|
|
60
|
+
const resolved = fs.realpathSync.native(value);
|
|
61
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function contractAckMatches(assignmentId, cwd, turnEcho, expectedWorkspacePath) {
|
|
68
|
+
if (!turnEcho.contract_hash || !turnEcho.capability_snapshot_hash)
|
|
69
|
+
return true;
|
|
70
|
+
const parsed = readContractAck(cwd, assignmentId, turnEcho.run_id);
|
|
71
|
+
const expectedWorkspace = expectedWorkspacePath ? normalizeWorkspacePath(expectedWorkspacePath) : undefined;
|
|
72
|
+
return parsed?.status === 'accepted'
|
|
73
|
+
&& parsed.turn_id === turnEcho.turn_id
|
|
74
|
+
&& parsed.run_id === turnEcho.run_id
|
|
75
|
+
&& parsed.nonce === turnEcho.nonce
|
|
76
|
+
&& parsed.contract_hash === turnEcho.contract_hash
|
|
77
|
+
&& parsed.capability_snapshot_hash === turnEcho.capability_snapshot_hash
|
|
78
|
+
&& (turnEcho.attempt_epoch === undefined || parsed.attempt_epoch === turnEcho.attempt_epoch)
|
|
79
|
+
&& (turnEcho.workspace_digest === undefined || parsed.workspace_digest === turnEcho.workspace_digest)
|
|
80
|
+
&& (!expectedWorkspace || parsed.cwd === expectedWorkspace);
|
|
52
81
|
}
|
|
53
82
|
// ── Helpers ────────────────────────────────────────────────
|
|
54
83
|
/** Parse a duration string like '4h', '30m', '1d' to milliseconds. */
|
|
@@ -149,8 +178,41 @@ export function executeDispatchedCommand(invoke, options) {
|
|
|
149
178
|
*/
|
|
150
179
|
export async function attemptExecution(invoke, options) {
|
|
151
180
|
const adapter = options.adapter ?? defaultExecutionAdapter;
|
|
181
|
+
const contracted = Boolean(options.turnEcho?.contract_hash && options.turnEcho.capability_snapshot_hash);
|
|
182
|
+
const fenceContractedGeneration = (reason, processInfo) => {
|
|
183
|
+
if (options.turnEcho) {
|
|
184
|
+
try {
|
|
185
|
+
recordExecutionContractAnomaly(options.turnEcho.run_id, {
|
|
186
|
+
source: 'bootstrap_ack',
|
|
187
|
+
reason,
|
|
188
|
+
}, options.cwd);
|
|
189
|
+
}
|
|
190
|
+
catch { /* a rejected/missing ack remains the fallback fence */ }
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
execution_status: processInfo?.pid ? 'delivered_and_started' : 'inbox_only',
|
|
194
|
+
pid: processInfo?.pid,
|
|
195
|
+
started_at: processInfo?.started_at,
|
|
196
|
+
error: `${reason}; contracted generation is fenced and MUST NOT be respawned`,
|
|
197
|
+
failure_kind: 'contract_acceptance_anomaly',
|
|
198
|
+
execution_reason: 'contract_acceptance_anomaly',
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
const prepareManual = () => {
|
|
202
|
+
// A custom adapter cannot self-attest that its opaque command contains the
|
|
203
|
+
// native bootstrap/env/sentinel fence. Until the adapter contract becomes
|
|
204
|
+
// declarative, only the core adapter may emit contracted manual launches.
|
|
205
|
+
if (contracted && adapter !== defaultExecutionAdapter)
|
|
206
|
+
return undefined;
|
|
207
|
+
const manual = adapter.prepareManualCommand(invoke, { ...options, ackRoot: options.cwd });
|
|
208
|
+
if (contracted && !manual.contractWrapped)
|
|
209
|
+
return undefined;
|
|
210
|
+
return manual;
|
|
211
|
+
};
|
|
152
212
|
// No invoke command available (IDE-only agents, etc.)
|
|
153
213
|
if (!invoke) {
|
|
214
|
+
if (contracted)
|
|
215
|
+
return fenceContractedGeneration('no contract-capable invoke command is available after crossing');
|
|
154
216
|
return { execution_status: 'inbox_only', execution_reason: 'no_invoke_command' };
|
|
155
217
|
}
|
|
156
218
|
const spawnCheck = adapter.canSpawn(options.agent);
|
|
@@ -162,7 +224,9 @@ export async function attemptExecution(invoke, options) {
|
|
|
162
224
|
// (1) autoExecute explicitly disabled: a deliberate manual handoff, NOT a
|
|
163
225
|
// failure. Prepend BRAINCLAW_CLAIM_ID so manual copy-paste still routes.
|
|
164
226
|
if (!options.autoExecute) {
|
|
165
|
-
const manual =
|
|
227
|
+
const manual = prepareManual();
|
|
228
|
+
if (!manual)
|
|
229
|
+
return fenceContractedGeneration('execution adapter cannot produce a contract-wrapped manual command');
|
|
166
230
|
return {
|
|
167
231
|
execution_status: 'command_ready_manual',
|
|
168
232
|
command: manual.command,
|
|
@@ -174,7 +238,9 @@ export async function attemptExecution(invoke, options) {
|
|
|
174
238
|
// failure of the caller's intent. Surface spawnCheck.reason (previously
|
|
175
239
|
// dropped) instead of returning a bare manual command.
|
|
176
240
|
if (!spawnCheck.canSpawn) {
|
|
177
|
-
const manual =
|
|
241
|
+
const manual = prepareManual();
|
|
242
|
+
if (!manual)
|
|
243
|
+
return fenceContractedGeneration(`agent is not spawnable and the adapter cannot produce a contract-wrapped manual command: ${spawnCheck.reason}`);
|
|
178
244
|
return {
|
|
179
245
|
execution_status: 'command_ready_manual',
|
|
180
246
|
command: manual.command,
|
|
@@ -200,7 +266,9 @@ export async function attemptExecution(invoke, options) {
|
|
|
200
266
|
scope: options.agent,
|
|
201
267
|
after: { reason: 'no_worktree', refused: true },
|
|
202
268
|
}, options.cwd);
|
|
203
|
-
const manual =
|
|
269
|
+
const manual = prepareManual();
|
|
270
|
+
if (!manual)
|
|
271
|
+
return fenceContractedGeneration('worktree is missing and the adapter cannot produce a contract-wrapped manual command');
|
|
204
272
|
return {
|
|
205
273
|
execution_status: 'command_ready_manual',
|
|
206
274
|
command: manual.command,
|
|
@@ -223,7 +291,9 @@ export async function attemptExecution(invoke, options) {
|
|
|
223
291
|
scope: options.agent,
|
|
224
292
|
after: { reason: instanceCheck.reason, active_sessions: instanceCheck.activeSessions, skipped: true },
|
|
225
293
|
}, options.cwd);
|
|
226
|
-
const manual =
|
|
294
|
+
const manual = prepareManual();
|
|
295
|
+
if (!manual)
|
|
296
|
+
return fenceContractedGeneration(`capacity is exhausted and the adapter cannot produce a contract-wrapped manual command: ${instanceCheck.reason}`);
|
|
227
297
|
return {
|
|
228
298
|
execution_status: 'command_ready_manual',
|
|
229
299
|
command: manual.command,
|
|
@@ -251,7 +321,7 @@ export async function attemptExecution(invoke, options) {
|
|
|
251
321
|
const parsedEnvTimeout = envTimeout ? Number.parseInt(envTimeout, 10) : NaN;
|
|
252
322
|
const handshakeTimeoutMs = options.handshakeTimeoutMs ??
|
|
253
323
|
(Number.isFinite(parsedEnvTimeout) && parsedEnvTimeout > 0 ? parsedEnvTimeout : 30_000);
|
|
254
|
-
const handshakeOk = await waitForAssignmentHandshake(options.assignmentId, options.cwd, handshakeTimeoutMs);
|
|
324
|
+
const handshakeOk = await waitForAssignmentHandshake(options.assignmentId, options.cwd, handshakeTimeoutMs, options.turnEcho?.run_id);
|
|
255
325
|
if (!handshakeOk) {
|
|
256
326
|
appendAuditEntry({
|
|
257
327
|
actor: options.dispatcherAgent,
|
|
@@ -262,7 +332,10 @@ export async function attemptExecution(invoke, options) {
|
|
|
262
332
|
scope: options.agent,
|
|
263
333
|
after: { reason: `No assignment handshake within ${handshakeTimeoutMs}ms`, pid: result.pid, command: invoke.bashCommand },
|
|
264
334
|
}, options.cwd);
|
|
265
|
-
|
|
335
|
+
if (contracted) {
|
|
336
|
+
return fenceContractedGeneration(`spawn launched but contract bootstrap did not acknowledge within ${handshakeTimeoutMs}ms`, { pid: result.pid, started_at: result.started_at });
|
|
337
|
+
}
|
|
338
|
+
const manual = prepareManual();
|
|
266
339
|
return {
|
|
267
340
|
execution_status: 'command_ready_manual',
|
|
268
341
|
command: manual.command,
|
|
@@ -273,6 +346,43 @@ export async function attemptExecution(invoke, options) {
|
|
|
273
346
|
pid: result.pid,
|
|
274
347
|
};
|
|
275
348
|
}
|
|
349
|
+
if (options.turnEcho && !contractAckMatches(options.assignmentId, options.cwd, options.turnEcho, options.worktreePath)) {
|
|
350
|
+
const accepted = readContractAck(options.cwd, options.assignmentId, options.turnEcho.run_id);
|
|
351
|
+
let anomalyPersistenceError;
|
|
352
|
+
try {
|
|
353
|
+
recordExecutionContractAnomaly(options.turnEcho.run_id, {
|
|
354
|
+
source: 'bootstrap_ack',
|
|
355
|
+
reason: 'bootstrap rejected, omitted or changed the immutable execution contract',
|
|
356
|
+
accepted_contract_hash: accepted?.contract_hash,
|
|
357
|
+
accepted_capability_snapshot_hash: accepted?.capability_snapshot_hash,
|
|
358
|
+
}, options.cwd);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
anomalyPersistenceError = error instanceof Error ? error.message : String(error);
|
|
362
|
+
}
|
|
363
|
+
appendAuditEntry({
|
|
364
|
+
actor: options.dispatcherAgent,
|
|
365
|
+
actor_id: options.dispatcherAgentId,
|
|
366
|
+
action: 'spawn_failed',
|
|
367
|
+
item_id: options.assignmentId,
|
|
368
|
+
item_type: 'agent_run',
|
|
369
|
+
scope: options.agent,
|
|
370
|
+
after: {
|
|
371
|
+
reason: 'post-crossing execution-contract acceptance mismatch or missing ack; respawn=false',
|
|
372
|
+
pid: result.pid,
|
|
373
|
+
accepted,
|
|
374
|
+
anomaly_persistence_error: anomalyPersistenceError,
|
|
375
|
+
},
|
|
376
|
+
}, options.cwd);
|
|
377
|
+
return {
|
|
378
|
+
execution_status: 'delivered_and_started',
|
|
379
|
+
started_at: result.started_at,
|
|
380
|
+
pid: result.pid,
|
|
381
|
+
error: `Worker/bootstrap did not acknowledge execution contract ${options.turnEcho.contract_hash ?? 'legacy'} exactly; run is anomalous and MUST NOT be respawned`,
|
|
382
|
+
failure_kind: 'contract_acceptance_anomaly',
|
|
383
|
+
execution_reason: 'contract_acceptance_anomaly',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
276
386
|
}
|
|
277
387
|
// Audit success
|
|
278
388
|
appendAuditEntry({
|
|
@@ -303,8 +413,12 @@ export async function attemptExecution(invoke, options) {
|
|
|
303
413
|
scope: options.agent,
|
|
304
414
|
after: { error: errorMsg, command: invoke.bashCommand },
|
|
305
415
|
}, options.cwd);
|
|
306
|
-
//
|
|
307
|
-
|
|
416
|
+
// Once a contracted generation crossed, an uncertain spawn outcome can
|
|
417
|
+
// never degrade to a second/manual launch. Legacy dispatch keeps fallback.
|
|
418
|
+
if (contracted) {
|
|
419
|
+
return fenceContractedGeneration(`spawn failed after the launch fence crossed (${errorMsg})`);
|
|
420
|
+
}
|
|
421
|
+
const manual = prepareManual();
|
|
308
422
|
return {
|
|
309
423
|
execution_status: 'command_ready_manual',
|
|
310
424
|
command: manual.command,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { LoopLinksSchema } from './loops/types.js';
|
|
2
3
|
export const ExecutionStatusSchema = z.enum(['delivered_and_started', 'command_ready_manual', 'inbox_only']);
|
|
3
4
|
export const WorkIntentSchema = z.enum(['execute', 'consult', 'resume', 'review']);
|
|
4
5
|
// pln#626 — coordinate intents split into three honest contracts:
|
|
@@ -37,6 +38,8 @@ export const CoordinateRequestSchema = z.object({
|
|
|
37
38
|
targetAgents: z.array(z.string()).optional(),
|
|
38
39
|
constraints: z.record(z.string(), z.unknown()).optional(),
|
|
39
40
|
threadId: z.string().optional(),
|
|
41
|
+
/** Optional pipeline provenance persisted when open_loop creates a loop. */
|
|
42
|
+
linked: LoopLinksSchema.optional(),
|
|
40
43
|
autoExecute: z.boolean().optional(),
|
|
41
44
|
/**
|
|
42
45
|
* When intent=review and open_loop=true, a review Loop is opened on top of
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { buildInvokeCommand, getCapabilityProfile } from '../agent-capability.js';
|
|
2
|
+
import { resolveBinaryOnPath } from '../execution-adapters.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const TerminalResultClaimSchema = z.object({
|
|
5
|
+
schema_version: z.literal(1),
|
|
6
|
+
status: z.enum(['completed', 'blocked', 'failed', 'partial']),
|
|
7
|
+
summary: z.string().min(1),
|
|
8
|
+
body: z.string().min(1).optional(),
|
|
9
|
+
artifact_type: z.string().min(1).optional(),
|
|
10
|
+
review_verdict: z.enum(['approve', 'request_changes']).optional(),
|
|
11
|
+
}).strict();
|
|
12
|
+
const TERMINAL_RESULT_PROTOCOL = `
|
|
13
|
+
|
|
14
|
+
Brainclaw native terminal result contract:
|
|
15
|
+
Your final assistant message MUST be exactly one JSON object with this shape and no markdown fence:
|
|
16
|
+
{"schema_version":1,"status":"completed|blocked|failed|partial","summary":"...","body":"optional details","artifact_type":"optional Loop artifact type","review_verdict":"approve|request_changes (required for review verdict work)"}
|
|
17
|
+
Do not infer or omit review_verdict when the task requires a review verdict.`;
|
|
18
|
+
export function withTerminalResultProtocol(input) {
|
|
19
|
+
return { ...input, prompt: `${input.prompt}${TERMINAL_RESULT_PROTOCOL}` };
|
|
20
|
+
}
|
|
21
|
+
export function parseTerminalResultClaim(text) {
|
|
22
|
+
const parsed = parseJsonObject(text.trim());
|
|
23
|
+
const claim = TerminalResultClaimSchema.safeParse(parsed);
|
|
24
|
+
if (!claim.success)
|
|
25
|
+
return undefined;
|
|
26
|
+
return { ...claim.data, raw_output_refs: [], diagnostics: [] };
|
|
27
|
+
}
|
|
28
|
+
export function applyTerminalResultClaim(base, terminalText) {
|
|
29
|
+
const structured = terminalText ? parseTerminalResultClaim(terminalText) : undefined;
|
|
30
|
+
if (!structured) {
|
|
31
|
+
base.diagnostics.push({
|
|
32
|
+
kind: 'protocol', code: 'invalid_result_claim',
|
|
33
|
+
message: 'terminal assistant output was not a strict Brainclaw result-claim v1 object',
|
|
34
|
+
});
|
|
35
|
+
if (base.status === 'completed')
|
|
36
|
+
base.status = 'partial';
|
|
37
|
+
return base;
|
|
38
|
+
}
|
|
39
|
+
if (base.status !== 'completed') {
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
summary: structured.summary,
|
|
43
|
+
body: structured.body,
|
|
44
|
+
artifact_type: structured.artifact_type,
|
|
45
|
+
review_verdict: structured.review_verdict,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...base,
|
|
50
|
+
status: structured.status,
|
|
51
|
+
summary: structured.summary,
|
|
52
|
+
body: structured.body,
|
|
53
|
+
artifact_type: structured.artifact_type,
|
|
54
|
+
review_verdict: structured.review_verdict,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function declaredProbe(adapterId, adapterVersion, agent, protocols, options = {}, requireInstalledExecutable = false) {
|
|
58
|
+
const profile = getCapabilityProfile(agent);
|
|
59
|
+
const executable = profile?.invoke_binary;
|
|
60
|
+
const resolvedExecutable = executable && requireInstalledExecutable
|
|
61
|
+
? (options.resolveExecutable ?? resolveBinaryOnPath)(executable)
|
|
62
|
+
: executable;
|
|
63
|
+
const available = Boolean(profile?.runtime.canBeSpawnedCli && executable && resolvedExecutable);
|
|
64
|
+
return {
|
|
65
|
+
adapter_id: adapterId,
|
|
66
|
+
adapter_version: adapterVersion,
|
|
67
|
+
agent,
|
|
68
|
+
executable,
|
|
69
|
+
availability: available ? 'declared' : 'unavailable',
|
|
70
|
+
supported_output_protocols: protocols,
|
|
71
|
+
model_attestation: profile?.model_flag ? 'cli_selectable' : 'unattested',
|
|
72
|
+
diagnostics: !profile
|
|
73
|
+
? [`unknown agent profile: ${agent}`]
|
|
74
|
+
: requireInstalledExecutable && executable && !resolvedExecutable
|
|
75
|
+
? [`executable not found on PATH: ${executable}`]
|
|
76
|
+
: [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function resolveDeclaredBinding(probe, requestedModel, rejectModel) {
|
|
80
|
+
if (probe.availability === 'unavailable') {
|
|
81
|
+
throw new Error(`harness_capability_rejected: ${probe.adapter_id} is unavailable for ${probe.agent}`);
|
|
82
|
+
}
|
|
83
|
+
const rejection = requestedModel && rejectModel?.(requestedModel);
|
|
84
|
+
if (rejection)
|
|
85
|
+
throw new Error(`harness_capability_rejected: ${rejection}`);
|
|
86
|
+
return {
|
|
87
|
+
adapter_id: probe.adapter_id,
|
|
88
|
+
adapter_version: probe.adapter_version,
|
|
89
|
+
agent: probe.agent,
|
|
90
|
+
requested_model: requestedModel,
|
|
91
|
+
resolved_model: requestedModel,
|
|
92
|
+
// A CLI model flag proves selection intent, not that the installed account
|
|
93
|
+
// can serve that name. We pass the exact string and never configure a
|
|
94
|
+
// fallback, but keep the resolution honest until runtime observes it.
|
|
95
|
+
model_resolution: requestedModel ? 'unattested' : 'defaulted',
|
|
96
|
+
probe,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function buildProfileInvocation(input) {
|
|
100
|
+
const invoke = buildInvokeCommand(input.binding.agent, input.prompt, {
|
|
101
|
+
mode: input.mode,
|
|
102
|
+
platform: input.platform,
|
|
103
|
+
model: input.binding.resolved_model,
|
|
104
|
+
});
|
|
105
|
+
if (!invoke)
|
|
106
|
+
throw new Error(`harness_prepare_failed: no invoke command for ${input.binding.agent}`);
|
|
107
|
+
return {
|
|
108
|
+
adapter_id: input.binding.adapter_id,
|
|
109
|
+
adapter_version: input.binding.adapter_version,
|
|
110
|
+
invoke,
|
|
111
|
+
output_protocol: 'text',
|
|
112
|
+
requested_model: input.binding.requested_model,
|
|
113
|
+
resolved_model: input.binding.resolved_model,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function genericOutcome(observation) {
|
|
117
|
+
const diagnostics = [];
|
|
118
|
+
if (observation.timed_out)
|
|
119
|
+
diagnostics.push({ kind: 'transport', code: 'timeout', message: 'execution timed out' });
|
|
120
|
+
if (observation.cancelled)
|
|
121
|
+
diagnostics.push({ kind: 'transport', code: 'cancelled', message: 'execution was cancelled' });
|
|
122
|
+
if (observation.exit_code === undefined) {
|
|
123
|
+
diagnostics.push({ kind: 'transport', code: 'unknown_exit', message: 'process exit code was not observed' });
|
|
124
|
+
}
|
|
125
|
+
else if (observation.exit_code !== 0) {
|
|
126
|
+
diagnostics.push({ kind: 'transport', code: 'nonzero_exit', message: `process exited with ${observation.exit_code}` });
|
|
127
|
+
}
|
|
128
|
+
const text = observation.stdout.trim() || observation.stderr.trim();
|
|
129
|
+
return {
|
|
130
|
+
status: observation.timed_out || observation.cancelled || (observation.exit_code !== undefined && observation.exit_code !== 0)
|
|
131
|
+
? 'failed'
|
|
132
|
+
: observation.exit_code === 0 ? 'completed' : 'partial',
|
|
133
|
+
summary: text.slice(0, 1000) || 'harness completed without terminal text',
|
|
134
|
+
body: text || undefined,
|
|
135
|
+
raw_output_refs: [],
|
|
136
|
+
diagnostics,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function parseJsonObject(text) {
|
|
140
|
+
try {
|
|
141
|
+
const value = JSON.parse(text);
|
|
142
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
143
|
+
? value
|
|
144
|
+
: undefined;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { applyTerminalResultClaim, buildProfileInvocation, declaredProbe, genericOutcome, parseJsonObject, resolveDeclaredBinding, withTerminalResultProtocol } from './base.js';
|
|
2
|
+
export class ClaudeHarnessAdapter {
|
|
3
|
+
id = 'claude-cli';
|
|
4
|
+
version = '1';
|
|
5
|
+
matches(agent) { return ['claude-code', 'claude-sonnet'].includes(agent.trim().toLowerCase()); }
|
|
6
|
+
probe(agent, options) {
|
|
7
|
+
return declaredProbe(this.id, this.version, agent, ['json', 'text'], options, true);
|
|
8
|
+
}
|
|
9
|
+
resolve(agent, requestedModel, options) {
|
|
10
|
+
return resolveDeclaredBinding(this.probe(agent, options), requestedModel, (model) => /fable/i.test(model) ? `Claude model '${model}' is not attested by the installed harness` : undefined);
|
|
11
|
+
}
|
|
12
|
+
prepare(input) {
|
|
13
|
+
const prepared = buildProfileInvocation(withTerminalResultProtocol(input));
|
|
14
|
+
prepared.invoke.args.push('--output-format', 'json');
|
|
15
|
+
prepared.invoke.bashCommand += ' --output-format "json"';
|
|
16
|
+
return { ...prepared, output_protocol: 'json' };
|
|
17
|
+
}
|
|
18
|
+
parseOutcome(observation) {
|
|
19
|
+
const base = genericOutcome(observation);
|
|
20
|
+
const parsed = parseJsonObject(observation.stdout.trim());
|
|
21
|
+
const terminalSuccess = parsed?.type === 'result'
|
|
22
|
+
&& parsed.subtype === 'success'
|
|
23
|
+
&& parsed.is_error === false
|
|
24
|
+
&& typeof parsed.result === 'string';
|
|
25
|
+
const result = terminalSuccess ? parsed.result : undefined;
|
|
26
|
+
const observedModel = typeof parsed?.model === 'string' ? parsed.model : undefined;
|
|
27
|
+
if (!parsed && observation.stdout.trim()) {
|
|
28
|
+
base.diagnostics.push({ kind: 'protocol', code: 'invalid_json', message: 'Claude output was not valid JSON' });
|
|
29
|
+
}
|
|
30
|
+
else if (!terminalSuccess) {
|
|
31
|
+
base.diagnostics.push({ kind: 'protocol', code: 'missing_terminal_result', message: 'Claude did not emit a successful terminal result object' });
|
|
32
|
+
}
|
|
33
|
+
if (base.status === 'completed' && !terminalSuccess)
|
|
34
|
+
base.status = 'partial';
|
|
35
|
+
const claimed = terminalSuccess ? applyTerminalResultClaim(base, result) : base;
|
|
36
|
+
return { ...claimed, observed_model: observedModel };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=claude.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { applyTerminalResultClaim, buildProfileInvocation, declaredProbe, genericOutcome, parseJsonObject, resolveDeclaredBinding, withTerminalResultProtocol } from './base.js';
|
|
2
|
+
export class CodexHarnessAdapter {
|
|
3
|
+
id = 'codex-cli';
|
|
4
|
+
version = '1';
|
|
5
|
+
matches(agent) { return agent.trim().toLowerCase() === 'codex'; }
|
|
6
|
+
probe(agent, options) {
|
|
7
|
+
return declaredProbe(this.id, this.version, agent, ['jsonl', 'text'], options, true);
|
|
8
|
+
}
|
|
9
|
+
resolve(agent, requestedModel, options) {
|
|
10
|
+
return resolveDeclaredBinding(this.probe(agent, options), requestedModel);
|
|
11
|
+
}
|
|
12
|
+
prepare(input) {
|
|
13
|
+
const prepared = buildProfileInvocation(withTerminalResultProtocol(input));
|
|
14
|
+
prepared.invoke.args.push('--json');
|
|
15
|
+
prepared.invoke.bashCommand += ' --json';
|
|
16
|
+
return { ...prepared, output_protocol: 'jsonl' };
|
|
17
|
+
}
|
|
18
|
+
parseOutcome(observation) {
|
|
19
|
+
const base = genericOutcome(observation);
|
|
20
|
+
const lines = observation.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
21
|
+
const parsed = lines.map((line) => ({ line, value: parseJsonObject(line) }));
|
|
22
|
+
const objects = parsed.map((item) => item.value).filter((item) => Boolean(item));
|
|
23
|
+
const invalidCount = parsed.filter((item) => !item.value).length;
|
|
24
|
+
const agentMessages = objects.flatMap((event) => {
|
|
25
|
+
const item = event.item;
|
|
26
|
+
if (event.type !== 'item.completed' || !item || typeof item !== 'object')
|
|
27
|
+
return [];
|
|
28
|
+
const record = item;
|
|
29
|
+
return record.type === 'agent_message' && typeof record.text === 'string' ? [record.text] : [];
|
|
30
|
+
});
|
|
31
|
+
const result = agentMessages.at(-1);
|
|
32
|
+
const terminalSuccess = objects.some((event) => event.type === 'turn.completed');
|
|
33
|
+
const reversed = [...objects].reverse();
|
|
34
|
+
const failure = reversed.find((event) => event.type === 'turn.failed')
|
|
35
|
+
?? reversed.find((event) => event.type === 'error');
|
|
36
|
+
const failureMessage = typeof failure?.message === 'string'
|
|
37
|
+
? failure.message
|
|
38
|
+
: failure?.error && typeof failure.error === 'object' && typeof failure.error.message === 'string'
|
|
39
|
+
? failure.error.message
|
|
40
|
+
: undefined;
|
|
41
|
+
if (invalidCount > 0) {
|
|
42
|
+
base.diagnostics.push({ kind: 'protocol', code: 'invalid_jsonl', message: `${invalidCount} Codex output line(s) were not valid JSON objects` });
|
|
43
|
+
}
|
|
44
|
+
if (failure) {
|
|
45
|
+
base.diagnostics.push({ kind: 'protocol', code: 'terminal_failure', message: failureMessage ?? 'Codex emitted a terminal failure event' });
|
|
46
|
+
base.status = 'failed';
|
|
47
|
+
}
|
|
48
|
+
else if (base.status === 'completed' && (!terminalSuccess || !result || invalidCount > 0)) {
|
|
49
|
+
base.diagnostics.push({ kind: 'protocol', code: 'missing_terminal_result', message: 'Codex did not emit a complete successful terminal result' });
|
|
50
|
+
base.status = 'partial';
|
|
51
|
+
}
|
|
52
|
+
if (failure)
|
|
53
|
+
return { ...base, summary: failureMessage?.slice(0, 1000) ?? base.summary };
|
|
54
|
+
return applyTerminalResultClaim(base, result);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=codex.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { loadAgentRun, recordRuntimeCapabilityObservation } from '../agentruns.js';
|
|
3
|
+
import { loadAssignment } from '../assignments.js';
|
|
4
|
+
import { findReservationByAssignmentId } from '../loops/attempt-reservation.js';
|
|
5
|
+
import { executionContractForGeneration } from '../loops/attempt-authority.js';
|
|
6
|
+
import { resolveTurnGenerationChain } from '../loops/attempt-generations.js';
|
|
7
|
+
import { getRuntimeLogPath, readCompletionSignals, readContractAck } from '../runtime-signals.js';
|
|
8
|
+
import { normalizeHarnessClaimToLaneResult, parseHarnessOutcome } from './result.js';
|
|
9
|
+
function readLog(file) {
|
|
10
|
+
try {
|
|
11
|
+
return fs.readFileSync(file, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Convert terminal native-harness logs into the existing untrusted LaneResult
|
|
19
|
+
* ingress. Reconciliation still owns identity checks, evidence sealing and
|
|
20
|
+
* every protocol gate.
|
|
21
|
+
*/
|
|
22
|
+
export function harvestHarnessObservation(assignmentId, cwd, persist = true) {
|
|
23
|
+
const assignment = loadAssignment(assignmentId, cwd);
|
|
24
|
+
const reservation = findReservationByAssignmentId(assignmentId, cwd);
|
|
25
|
+
const resolvedGeneration = reservation ? resolveTurnGenerationChain(cwd, reservation.turn_id) : undefined;
|
|
26
|
+
const generation = resolvedGeneration && (resolvedGeneration.status === 'active' || resolvedGeneration.status === 'settled')
|
|
27
|
+
? resolvedGeneration.latest_generation
|
|
28
|
+
: undefined;
|
|
29
|
+
const generationContract = reservation && generation
|
|
30
|
+
? executionContractForGeneration(reservation, generation)
|
|
31
|
+
: undefined;
|
|
32
|
+
const ref = generationContract?.ref ?? reservation?.execution_contract_ref;
|
|
33
|
+
const reservedHarness = reservation?.capability_snapshot?.resolved.harness;
|
|
34
|
+
const projectedHarness = assignment?.capability_snapshot?.resolved.harness;
|
|
35
|
+
if (reservedHarness && projectedHarness && JSON.stringify(reservedHarness) !== JSON.stringify(projectedHarness)) {
|
|
36
|
+
throw new Error(`assignment ${assignmentId} harness binding diverges from the authoritative reservation`);
|
|
37
|
+
}
|
|
38
|
+
const harness = reservedHarness ?? projectedHarness;
|
|
39
|
+
if (!assignment || !reservation || !ref || !harness || harness.adapter_id === 'prompt-only')
|
|
40
|
+
return undefined;
|
|
41
|
+
const signals = readCompletionSignals(cwd, assignmentId, generation?.run_id);
|
|
42
|
+
const terminal = signals.completed ?? signals.failed;
|
|
43
|
+
if (!terminal)
|
|
44
|
+
return undefined;
|
|
45
|
+
const stdoutLog = getRuntimeLogPath(cwd, assignmentId, 'stdout', generation?.run_id);
|
|
46
|
+
const stderrLog = getRuntimeLogPath(cwd, assignmentId, 'stderr', generation?.run_id);
|
|
47
|
+
const bothTerminalSignals = Boolean(signals.completed && signals.failed);
|
|
48
|
+
const observation = {
|
|
49
|
+
exit_code: bothTerminalSignals ? undefined : signals.completed ? 0 : 1,
|
|
50
|
+
stdout: readLog(stdoutLog),
|
|
51
|
+
stderr: readLog(stderrLog),
|
|
52
|
+
completed_at: terminal.at,
|
|
53
|
+
};
|
|
54
|
+
const claim = parseHarnessOutcome(harness.adapter_id, observation, harness.adapter_version);
|
|
55
|
+
claim.raw_output_refs = [stdoutLog, stderrLog];
|
|
56
|
+
if (reservation.execution_contract?.identity.kind === 'review'
|
|
57
|
+
&& claim.status === 'completed'
|
|
58
|
+
&& !claim.review_verdict) {
|
|
59
|
+
claim.status = 'partial';
|
|
60
|
+
claim.diagnostics.push({
|
|
61
|
+
kind: 'protocol', code: 'missing_review_verdict',
|
|
62
|
+
message: 'review result-claim v1 must carry review_verdict=approve|request_changes',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
claim.artifact_type ??= reservation.expected_artifacts.find((item) => item.completion_policy === 'required')?.loop_artifact_type
|
|
66
|
+
?? reservation.expected_artifacts[0]?.loop_artifact_type;
|
|
67
|
+
const protocolDiagnostics = claim.diagnostics.filter((item) => item.kind === 'protocol');
|
|
68
|
+
const protocolStatus = claim.status === 'partial'
|
|
69
|
+
? 'partial'
|
|
70
|
+
: protocolDiagnostics.length > 0 ? 'invalid' : claim.body ? 'valid' : 'absent';
|
|
71
|
+
const ack = readContractAck(cwd, assignmentId, generation?.run_id);
|
|
72
|
+
const missingHash = '0'.repeat(64);
|
|
73
|
+
const runtimeObservation = {
|
|
74
|
+
contract_hash: terminal.contract_hash ?? missingHash,
|
|
75
|
+
capability_snapshot_hash: terminal.capability_snapshot_hash ?? missingHash,
|
|
76
|
+
adapter_id: harness.adapter_id,
|
|
77
|
+
adapter_version: harness.adapter_version,
|
|
78
|
+
observed_surfaces: ['cli'],
|
|
79
|
+
observed_model: claim.observed_model,
|
|
80
|
+
accepted_contract_hash: ack?.contract_hash,
|
|
81
|
+
accepted_capability_snapshot_hash: ack?.capability_snapshot_hash,
|
|
82
|
+
};
|
|
83
|
+
const activeRunId = generation?.run_id ?? reservation.child_ids.run_id;
|
|
84
|
+
const run = loadAgentRun(activeRunId, cwd);
|
|
85
|
+
if (run && persist) {
|
|
86
|
+
recordRuntimeCapabilityObservation(run.id, runtimeObservation, {
|
|
87
|
+
adapter_id: harness.adapter_id,
|
|
88
|
+
adapter_version: harness.adapter_version,
|
|
89
|
+
transport_status: bothTerminalSignals ? 'failed' : signals.completed ? 'completed' : 'failed',
|
|
90
|
+
protocol_status: protocolStatus,
|
|
91
|
+
message: claim.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ') || undefined,
|
|
92
|
+
}, cwd);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
lane: normalizeHarnessClaimToLaneResult(claim, {
|
|
96
|
+
assignment_id: assignmentId,
|
|
97
|
+
turn_id: reservation.turn_id,
|
|
98
|
+
run_id: activeRunId,
|
|
99
|
+
nonce: terminal.nonce,
|
|
100
|
+
attempt_epoch: terminal.attempt_epoch,
|
|
101
|
+
workspace_digest: terminal.workspace_digest,
|
|
102
|
+
execution_contract_hash: terminal.contract_hash,
|
|
103
|
+
capability_snapshot_hash: terminal.capability_snapshot_hash,
|
|
104
|
+
}),
|
|
105
|
+
stdout_log: stdoutLog,
|
|
106
|
+
stderr_log: stderrLog,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=harvest.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { buildProfileInvocation, declaredProbe, genericOutcome, resolveDeclaredBinding } from './base.js';
|
|
2
|
+
export class PromptOnlyHarnessAdapter {
|
|
3
|
+
id = 'prompt-only';
|
|
4
|
+
version = '1';
|
|
5
|
+
matches() { return true; }
|
|
6
|
+
probe(agent, options) { return declaredProbe(this.id, this.version, agent, ['text'], options); }
|
|
7
|
+
resolve(agent, requestedModel, options) {
|
|
8
|
+
return resolveDeclaredBinding(this.probe(agent, options), requestedModel);
|
|
9
|
+
}
|
|
10
|
+
prepare = buildProfileInvocation;
|
|
11
|
+
parseOutcome = genericOutcome;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=prompt-only.js.map
|