brainclaw 1.26.2 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) 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 +87 -14
  8. package/dist/commands/mcp-catalog.js +42 -18
  9. package/dist/commands/mcp-schemas.generated.js +44 -0
  10. package/dist/commands/mcp-write-claims.js +128 -1
  11. package/dist/commands/mcp-write-coordination.js +146 -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 +160 -14
  25. package/dist/core/execution-contract.js +345 -0
  26. package/dist/core/execution.js +130 -16
  27. package/dist/core/harness-adapters/base.js +150 -0
  28. package/dist/core/harness-adapters/claude.js +39 -0
  29. package/dist/core/harness-adapters/codex.js +57 -0
  30. package/dist/core/harness-adapters/harvest.js +109 -0
  31. package/dist/core/harness-adapters/index.js +8 -0
  32. package/dist/core/harness-adapters/prompt-only.js +13 -0
  33. package/dist/core/harness-adapters/registry.js +48 -0
  34. package/dist/core/harness-adapters/result.js +33 -0
  35. package/dist/core/harness-adapters/types.js +2 -0
  36. package/dist/core/ideation-loop-close.js +25 -2
  37. package/dist/core/instruction-templates.js +3 -2
  38. package/dist/core/loop-turn-dispatch.js +207 -0
  39. package/dist/core/loops/artifact-contract.js +11 -0
  40. package/dist/core/loops/attempt-authority.js +476 -0
  41. package/dist/core/loops/attempt-generations.js +509 -0
  42. package/dist/core/loops/attempt-reservation.js +197 -35
  43. package/dist/core/loops/attempt-rollout.js +404 -0
  44. package/dist/core/loops/attempt-takeover.js +155 -0
  45. package/dist/core/loops/bootstrap-acquire.js +7 -3
  46. package/dist/core/loops/evidence.js +187 -0
  47. package/dist/core/loops/facade-schema.js +41 -10
  48. package/dist/core/loops/gate-policy.js +485 -0
  49. package/dist/core/loops/impl-bind.js +37 -79
  50. package/dist/core/loops/index.js +9 -0
  51. package/dist/core/loops/iteration-engine.js +31 -19
  52. package/dist/core/loops/kind-policies.js +90 -0
  53. package/dist/core/loops/lock.js +71 -13
  54. package/dist/core/loops/reconcile-turn.js +235 -18
  55. package/dist/core/loops/result-reducers.js +99 -10
  56. package/dist/core/loops/store.js +30 -3
  57. package/dist/core/loops/turn-execution.js +480 -0
  58. package/dist/core/loops/types.js +113 -2
  59. package/dist/core/loops/verbs.js +332 -99
  60. package/dist/core/loops/verify-command.js +31 -8
  61. package/dist/core/loops/workspace-digest.js +54 -0
  62. package/dist/core/review-loop-close.js +25 -3
  63. package/dist/core/review-loop-turn-dispatch.js +210 -161
  64. package/dist/core/runtime-signals.js +62 -25
  65. package/dist/core/schema.js +35 -0
  66. package/dist/core/spawn-check.js +3 -2
  67. package/dist/core/upgrades/backup.js +27 -4
  68. package/dist/facts.js +7 -6
  69. package/dist/facts.json +6 -5
  70. package/docs/cli.md +49 -1
  71. package/docs/concepts/attempt-authority.md +407 -0
  72. package/docs/concepts/evidence-attestations.md +135 -0
  73. package/docs/concepts/execution-contract.md +166 -0
  74. package/docs/concepts/harness-adapters.md +166 -0
  75. package/docs/concepts/ideation-loop.md +5 -4
  76. package/docs/concepts/loop-engine.md +302 -113
  77. package/docs/index.md +4 -1
  78. package/docs/integrations/codex.md +3 -3
  79. package/docs/integrations/mcp.md +59 -5
  80. package/docs/loops/debug.md +144 -0
  81. package/docs/loops/ideation.md +158 -0
  82. package/docs/loops/implementation.md +154 -0
  83. package/docs/loops/research.md +136 -0
  84. package/docs/loops/review.md +200 -0
  85. package/docs/mcp-schema-changelog.md +14 -5
  86. package/package.json +1 -1
@@ -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 = adapter.prepareManualCommand(invoke, options);
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 = adapter.prepareManualCommand(invoke, options);
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 = adapter.prepareManualCommand(invoke, options);
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 = adapter.prepareManualCommand(invoke, options);
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
- const manual = adapter.prepareManualCommand(invoke, options);
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
- // Graceful fallback include BRAINCLAW_CLAIM_ID for manual routing
307
- const manual = adapter.prepareManualCommand(invoke, options);
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,
@@ -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,8 @@
1
+ export * from './types.js';
2
+ export * from './prompt-only.js';
3
+ export * from './codex.js';
4
+ export * from './claude.js';
5
+ export * from './registry.js';
6
+ export * from './result.js';
7
+ export * from './harvest.js';
8
+ //# sourceMappingURL=index.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