brainclaw 1.26.2 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +13 -0
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-coordination.js +65 -1
  4. package/dist/commands/attempt-authority.js +80 -0
  5. package/dist/commands/harvest.js +140 -61
  6. package/dist/commands/loop.js +34 -0
  7. package/dist/commands/loops-handlers.js +143 -15
  8. package/dist/commands/mcp-catalog.js +52 -18
  9. package/dist/commands/mcp-schemas.generated.js +64 -0
  10. package/dist/commands/mcp-write-claims.js +128 -1
  11. package/dist/commands/mcp-write-coordination.js +149 -76
  12. package/dist/core/agent-capability.js +1 -1
  13. package/dist/core/agentrun-reconciler.js +148 -22
  14. package/dist/core/agentruns.js +254 -29
  15. package/dist/core/assignment-request-schema.js +7 -0
  16. package/dist/core/assignment-sweeper.js +5 -3
  17. package/dist/core/assignments.js +131 -33
  18. package/dist/core/claim-request-schema.js +7 -0
  19. package/dist/core/claims.js +53 -2
  20. package/dist/core/dispatch-status.js +16 -6
  21. package/dist/core/dispatcher.js +51 -51
  22. package/dist/core/entity-operations.js +20 -0
  23. package/dist/core/events.js +4 -0
  24. package/dist/core/execution-adapters.js +189 -14
  25. package/dist/core/execution-contract.js +345 -0
  26. package/dist/core/execution.js +130 -16
  27. package/dist/core/facade-schema.js +3 -0
  28. package/dist/core/harness-adapters/base.js +150 -0
  29. package/dist/core/harness-adapters/claude.js +39 -0
  30. package/dist/core/harness-adapters/codex.js +57 -0
  31. package/dist/core/harness-adapters/harvest.js +109 -0
  32. package/dist/core/harness-adapters/index.js +8 -0
  33. package/dist/core/harness-adapters/prompt-only.js +13 -0
  34. package/dist/core/harness-adapters/registry.js +48 -0
  35. package/dist/core/harness-adapters/result.js +33 -0
  36. package/dist/core/harness-adapters/types.js +2 -0
  37. package/dist/core/ideation-loop-close.js +25 -2
  38. package/dist/core/instruction-templates.js +3 -2
  39. package/dist/core/loop-turn-dispatch.js +235 -0
  40. package/dist/core/loops/artifact-contract.js +11 -0
  41. package/dist/core/loops/attempt-authority.js +496 -0
  42. package/dist/core/loops/attempt-generations.js +509 -0
  43. package/dist/core/loops/attempt-reservation.js +197 -35
  44. package/dist/core/loops/attempt-rollout.js +404 -0
  45. package/dist/core/loops/attempt-takeover.js +155 -0
  46. package/dist/core/loops/bootstrap-acquire.js +7 -3
  47. package/dist/core/loops/brief-assembly.js +21 -4
  48. package/dist/core/loops/evidence.js +188 -0
  49. package/dist/core/loops/facade-schema.js +75 -11
  50. package/dist/core/loops/gate-policy.js +533 -0
  51. package/dist/core/loops/impl-bind.js +91 -81
  52. package/dist/core/loops/index.js +9 -0
  53. package/dist/core/loops/iteration-engine.js +31 -19
  54. package/dist/core/loops/kind-policies.js +90 -0
  55. package/dist/core/loops/lock.js +71 -13
  56. package/dist/core/loops/reconcile-turn.js +237 -18
  57. package/dist/core/loops/result-reducers.js +113 -10
  58. package/dist/core/loops/store.js +34 -3
  59. package/dist/core/loops/turn-execution.js +480 -0
  60. package/dist/core/loops/types.js +127 -3
  61. package/dist/core/loops/verbs.js +335 -99
  62. package/dist/core/loops/verify-command.js +105 -20
  63. package/dist/core/loops/workspace-digest.js +54 -0
  64. package/dist/core/review-loop-close.js +25 -3
  65. package/dist/core/review-loop-turn-dispatch.js +210 -161
  66. package/dist/core/runtime-signals.js +62 -25
  67. package/dist/core/schema.js +40 -0
  68. package/dist/core/spawn-check.js +3 -2
  69. package/dist/core/upgrades/backup.js +27 -4
  70. package/dist/facts.js +9 -8
  71. package/dist/facts.json +8 -7
  72. package/docs/cli.md +49 -1
  73. package/docs/concepts/attempt-authority.md +407 -0
  74. package/docs/concepts/evidence-attestations.md +135 -0
  75. package/docs/concepts/execution-contract.md +166 -0
  76. package/docs/concepts/harness-adapters.md +166 -0
  77. package/docs/concepts/ideation-loop.md +5 -4
  78. package/docs/concepts/loop-engine.md +302 -113
  79. package/docs/index.md +4 -1
  80. package/docs/integrations/codex.md +3 -3
  81. package/docs/integrations/mcp.md +59 -5
  82. package/docs/loops/debug.md +144 -0
  83. package/docs/loops/ideation.md +158 -0
  84. package/docs/loops/implementation.md +174 -0
  85. package/docs/loops/research.md +136 -0
  86. package/docs/loops/review.md +200 -0
  87. package/docs/mcp-schema-changelog.md +18 -5
  88. package/package.json +1 -1
@@ -5,6 +5,56 @@ import { buildClaimEnvPrefix, buildWorkerIdentityEnv } from './execution-profile
5
5
  import { getCapabilityProfile } from './agent-capability.js';
6
6
  import { nowISO } from './ids.js';
7
7
  import { ensureRuntimeDirs, getRuntimeLogPath, getRuntimeSignalPath, } from './runtime-signals.js';
8
+ const CONTRACT_BOOTSTRAP_SOURCE = `'use strict';
9
+ const fs = require('node:fs');
10
+ const [ackPath, turnId, runId, nonce, expectedContractHash, expectedSnapshotHash, attemptEpoch, workspaceDigest, expectedWorkspaceB64] = process.argv.slice(2);
11
+ const contractHash = process.env.BRAINCLAW_EXECUTION_CONTRACT_HASH || '';
12
+ const snapshotHash = process.env.BRAINCLAW_CAPABILITY_SNAPSHOT_HASH || '';
13
+ const normalize = (value) => {
14
+ let resolved = fs.realpathSync.native(value);
15
+ if (process.platform === 'win32') resolved = resolved.toLowerCase();
16
+ return resolved;
17
+ };
18
+ const expectedWorkspace = Buffer.from(expectedWorkspaceB64 || '', 'base64url').toString('utf8');
19
+ const actualCwd = normalize(process.cwd());
20
+ const accepted = contractHash === expectedContractHash
21
+ && snapshotHash === expectedSnapshotHash
22
+ && expectedWorkspace !== ''
23
+ && actualCwd === normalize(expectedWorkspace);
24
+ let fd;
25
+ try {
26
+ fd = fs.openSync(ackPath, 'wx');
27
+ } catch (error) {
28
+ if (error && error.code === 'EEXIST') process.exit(79);
29
+ throw error;
30
+ }
31
+ try {
32
+ fs.writeFileSync(fd, JSON.stringify({
33
+ status: accepted ? 'accepted' : 'rejected',
34
+ turn_id: turnId,
35
+ run_id: runId,
36
+ nonce,
37
+ ...(attemptEpoch ? { attempt_epoch: Number(attemptEpoch) } : {}),
38
+ ...(workspaceDigest ? { workspace_digest: workspaceDigest } : {}),
39
+ contract_hash: contractHash,
40
+ capability_snapshot_hash: snapshotHash,
41
+ cwd: actualCwd,
42
+ }));
43
+ } finally {
44
+ fs.closeSync(fd);
45
+ }
46
+ if (!accepted) process.exitCode = 78;
47
+ `;
48
+ /** Materialize the tiny child bootstrap used by both cmd.exe and POSIX shells. */
49
+ export function writeContractBootstrapScript(scriptPath) {
50
+ fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
51
+ try {
52
+ if (fs.readFileSync(scriptPath, 'utf8') === CONTRACT_BOOTSTRAP_SOURCE)
53
+ return;
54
+ }
55
+ catch { /* create or replace below */ }
56
+ fs.writeFileSync(scriptPath, CONTRACT_BOOTSTRAP_SOURCE, 'utf8');
57
+ }
8
58
  // The turn-echo values are raw-embedded into a shell one-liner (see marker()),
9
59
  // so the `[A-Za-z0-9_-]` safety invariant documented on TurnEcho is LOAD-BEARING,
10
60
  // not cosmetic. A stray `"` desyncs cmd.exe quote-parity (no sentinel file is
@@ -18,7 +68,9 @@ const TURN_ECHO_SAFE = /^[A-Za-z0-9_-]+$/;
18
68
  export function buildAckWrapCommand(bashCommand, paths, isWin32, turnEcho) {
19
69
  if (turnEcho) {
20
70
  for (const [field, value] of Object.entries(turnEcho)) {
21
- if (!TURN_ECHO_SAFE.test(value)) {
71
+ if (value === undefined)
72
+ continue;
73
+ if (!TURN_ECHO_SAFE.test(String(value))) {
22
74
  throw new Error(`buildAckWrapCommand: turnEcho.${field} must match ${TURN_ECHO_SAFE} to be shell-safe for the completion sentinel (got ${JSON.stringify(value)})`);
23
75
  }
24
76
  }
@@ -32,12 +84,42 @@ export function buildAckWrapCommand(bashCommand, paths, isWin32, turnEcho) {
32
84
  const marker = (p, status) => {
33
85
  if (!turnEcho)
34
86
  return touch(p);
35
- const body = JSON.stringify({ turn_id: turnEcho.turn_id, run_id: turnEcho.run_id, nonce: turnEcho.nonce, status });
87
+ const body = JSON.stringify({
88
+ turn_id: turnEcho.turn_id,
89
+ run_id: turnEcho.run_id,
90
+ nonce: turnEcho.nonce,
91
+ ...(turnEcho.contract_hash ? { contract_hash: turnEcho.contract_hash } : {}),
92
+ ...(turnEcho.capability_snapshot_hash ? { capability_snapshot_hash: turnEcho.capability_snapshot_hash } : {}),
93
+ ...(turnEcho.attempt_epoch !== undefined ? { attempt_epoch: turnEcho.attempt_epoch } : {}),
94
+ ...(turnEcho.workspace_digest ? { workspace_digest: turnEcho.workspace_digest } : {}),
95
+ status,
96
+ });
36
97
  return isWin32 ? `echo ${body}>"${p}"` : `printf '%s' '${body}' > "${p}"`;
37
98
  };
38
- const redirected = `${bashCommand} > "${paths.stdoutLog}" 2> "${paths.stderrLog}"`;
39
- return (`${touch(paths.ackPath)} && ` +
40
- `( ${redirected} && ${marker(paths.completedPath, 'completed')} || ${marker(paths.failedPath, 'failed')} )`);
99
+ const ack = (() => {
100
+ if (!turnEcho?.contract_hash || !turnEcho.capability_snapshot_hash)
101
+ return touch(paths.ackPath);
102
+ if (!paths.contractBootstrapPath) {
103
+ throw new Error('buildAckWrapCommand: contracted turn requires contractBootstrapPath');
104
+ }
105
+ // This child reads the EFFECTIVE environment it received. It writes the
106
+ // actual values and exits non-zero on mismatch, so `&&` prevents worker exec.
107
+ if (!paths.expectedWorkspacePath) {
108
+ throw new Error('buildAckWrapCommand: contracted turn requires expectedWorkspacePath');
109
+ }
110
+ const expectedWorkspaceB64 = Buffer.from(paths.expectedWorkspacePath, 'utf8').toString('base64url');
111
+ return `"${process.execPath}" "${paths.contractBootstrapPath}" "${paths.ackPath}" "${turnEcho.turn_id}" "${turnEcho.run_id}" "${turnEcho.nonce}" "${turnEcho.contract_hash}" "${turnEcho.capability_snapshot_hash}" "${turnEcho.attempt_epoch ?? ''}" "${turnEcho.workspace_digest ?? ''}" "${expectedWorkspaceB64}"`;
112
+ })();
113
+ const stdinRedirect = paths.stdinFilePath ? ` < "${paths.stdinFilePath}"` : '';
114
+ const redirected = `${bashCommand}${stdinRedirect} > "${paths.stdoutLog}" 2> "${paths.stderrLog}"`;
115
+ const cleanup = paths.stdinFilePath
116
+ ? isWin32
117
+ ? ` & del /q "${paths.stdinFilePath}" > nul 2>&1`
118
+ : `; rm -f -- "${paths.stdinFilePath}"`
119
+ : '';
120
+ return (`${ack} && ` +
121
+ `( ${redirected} && ${marker(paths.completedPath, 'completed')} || ${marker(paths.failedPath, 'failed')} )` +
122
+ cleanup);
41
123
  }
42
124
  /**
43
125
  * Check if a binary is resolvable on the system PATH.
@@ -96,6 +178,33 @@ function buildManualEnvPrefix(claimId) {
96
178
  // wrapper for symmetry with the dispatcher's buildEnvPrefix.
97
179
  return buildClaimEnvPrefix(claimId);
98
180
  }
181
+ /**
182
+ * Make the isolated worktree the explicit Codex workspace root. Relying only
183
+ * on child_process.cwd is insufficient for non-interactive Windows launches:
184
+ * the Codex sandbox can retain the coordinator workspace and apply_patch then
185
+ * refuses writes in ~/.brainclaw/worktrees even when NTFS grants access.
186
+ * `--cd` defines the primary root. Do not redundantly add the same path with
187
+ * `--add-dir`: the unelevated Windows sandbox cannot enforce split writable
188
+ * root sets and refuses to prepare its wrapper in that configuration.
189
+ */
190
+ export function withCodexWorkspaceRoot(invoke, agent, worktreePath, isWin32 = process.platform === 'win32') {
191
+ const executableName = path.win32.basename(invoke.executable).replace(/\.(?:cmd|exe|bat|com)$/i, '').toLowerCase();
192
+ if (agent.trim().toLowerCase() !== 'codex' || executableName !== 'codex' || !worktreePath)
193
+ return invoke;
194
+ const args = [...invoke.args];
195
+ const subcommandIndex = args.indexOf('exec');
196
+ const insertAt = subcommandIndex >= 0 ? subcommandIndex : 0;
197
+ args.splice(insertAt, 0, '--cd', worktreePath);
198
+ const quote = (value) => isWin32
199
+ ? `"${value.replace(/"/g, '""')}"`
200
+ : `'${value.replace(/'/g, `'\\''`)}'`;
201
+ const flags = `--cd ${quote(worktreePath)}`;
202
+ const prefix = invoke.executable;
203
+ const suffix = invoke.bashCommand.startsWith(`${prefix} `)
204
+ ? invoke.bashCommand.slice(prefix.length + 1)
205
+ : invoke.bashCommand;
206
+ return { ...invoke, args, bashCommand: `${prefix} ${flags} ${suffix}` };
207
+ }
99
208
  export class CliExecutionAdapter {
100
209
  id = 'cli';
101
210
  canSpawn(agentName) {
@@ -115,14 +224,57 @@ export class CliExecutionAdapter {
115
224
  return { canSpawn: true, reason: 'agent has spawnable profile' };
116
225
  }
117
226
  prepareManualCommand(invoke, options) {
227
+ const isWin32 = process.platform === 'win32';
228
+ invoke = withCodexWorkspaceRoot(invoke, options.agent, options.worktreePath, isWin32);
229
+ const shell = isWin32 ? 'cmd' : (invoke.shell ? 'bash' : 'sh');
230
+ if (options.turnEcho?.contract_hash
231
+ && options.turnEcho.capability_snapshot_hash
232
+ && options.assignmentId
233
+ && (options.ackRoot ?? options.worktreePath)) {
234
+ const signalRoot = options.ackRoot ?? options.worktreePath;
235
+ ensureRuntimeDirs(signalRoot);
236
+ const runtimeRunId = options.turnEcho?.run_id;
237
+ const ackPath = getRuntimeSignalPath(signalRoot, options.assignmentId, 'ack', runtimeRunId);
238
+ const contractBootstrapPath = `${ackPath}.bootstrap.cjs`;
239
+ writeContractBootstrapScript(contractBootstrapPath);
240
+ const stdinFilePath = isWin32 && invoke.promptDelivery === 'stdin_pipe' && invoke.promptText
241
+ ? `${ackPath}.stdin`
242
+ : undefined;
243
+ if (stdinFilePath && invoke.promptText) {
244
+ fs.writeFileSync(stdinFilePath, invoke.promptText, { encoding: 'utf8', mode: 0o600 });
245
+ }
246
+ const wrapped = buildAckWrapCommand(invoke.bashCommand, {
247
+ ackPath,
248
+ completedPath: getRuntimeSignalPath(signalRoot, options.assignmentId, 'completed', runtimeRunId),
249
+ failedPath: getRuntimeSignalPath(signalRoot, options.assignmentId, 'failed', runtimeRunId),
250
+ stdoutLog: getRuntimeLogPath(signalRoot, options.assignmentId, 'stdout', runtimeRunId),
251
+ stderrLog: getRuntimeLogPath(signalRoot, options.assignmentId, 'stderr', runtimeRunId),
252
+ stdinFilePath,
253
+ contractBootstrapPath,
254
+ expectedWorkspacePath: options.worktreePath,
255
+ }, isWin32, options.turnEcho);
256
+ const contractEnv = isWin32
257
+ ? [
258
+ options.claimId && options.claimId !== '(dry-run)' ? `set BRAINCLAW_CLAIM_ID=${options.claimId}` : undefined,
259
+ `set BRAINCLAW_EXECUTION_CONTRACT_HASH=${options.turnEcho.contract_hash}`,
260
+ `set BRAINCLAW_CAPABILITY_SNAPSHOT_HASH=${options.turnEcho.capability_snapshot_hash}`,
261
+ ].filter((item) => Boolean(item)).join(' && ') + ' && '
262
+ : `export ${[
263
+ options.claimId && options.claimId !== '(dry-run)' ? `BRAINCLAW_CLAIM_ID="${options.claimId}"` : undefined,
264
+ `BRAINCLAW_EXECUTION_CONTRACT_HASH="${options.turnEcho.contract_hash}"`,
265
+ `BRAINCLAW_CAPABILITY_SNAPSHOT_HASH="${options.turnEcho.capability_snapshot_hash}"`,
266
+ ].filter((item) => Boolean(item)).join(' ')}; `;
267
+ return { command: `${contractEnv}${wrapped}`, shell, contractWrapped: true };
268
+ }
118
269
  const envPrefix = buildManualEnvPrefix(options.claimId);
119
270
  return {
120
271
  command: `${envPrefix}${invoke.bashCommand}`,
121
- shell: process.platform === 'win32' ? 'cmd' : (invoke.shell ? 'bash' : 'sh'),
272
+ shell,
122
273
  };
123
274
  }
124
275
  start(invoke, options) {
125
276
  const isWin32 = process.platform === 'win32';
277
+ invoke = withCodexWorkspaceRoot(invoke, options.agent, options.worktreePath, isWin32);
126
278
  // F7 (trp_0e5150d3): route worker env through buildWorkerIdentityEnv so the
127
279
  // worker is an independent agent — coordinator identity (BRAINCLAW_AGENT*,
128
280
  // SESSION_ID, PROJECT) is scrubbed LAST and cannot be reintroduced by
@@ -131,7 +283,16 @@ export class CliExecutionAdapter {
131
283
  const env = buildWorkerIdentityEnv(process.env, {
132
284
  agent: options.agent,
133
285
  claimId: options.claimId,
134
- extraEnv: { ...buildGitAttributionEnv(options.agent), ...(invoke.env ?? {}) },
286
+ extraEnv: {
287
+ ...buildGitAttributionEnv(options.agent),
288
+ ...(invoke.env ?? {}),
289
+ // Contract identity wins over any agent/invoke-provided environment.
290
+ // The child bootstrap independently verifies these effective values.
291
+ ...(options.turnEcho?.contract_hash ? { BRAINCLAW_EXECUTION_CONTRACT_HASH: options.turnEcho.contract_hash } : {}),
292
+ ...(options.turnEcho?.capability_snapshot_hash
293
+ ? { BRAINCLAW_CAPABILITY_SNAPSHOT_HASH: options.turnEcho.capability_snapshot_hash }
294
+ : {}),
295
+ },
135
296
  });
136
297
  if (invoke.promptDelivery === 'temp_file' && invoke.tempFilePath && invoke.promptText) {
137
298
  const dir = path.dirname(invoke.tempFilePath);
@@ -156,7 +317,8 @@ export class CliExecutionAdapter {
156
317
  // process just ignores stdout/stderr here. stdin stays a pipe when the
157
318
  // prompt is delivered that way (the grouped agent command inherits it).
158
319
  const useAckWrap = !!(options.assignmentId && (options.ackRoot ?? options.worktreePath));
159
- const stdinTarget = needsStdin ? 'pipe' : 'ignore';
320
+ const useWindowsStdinFile = isWin32 && useAckWrap && Boolean(needsStdin);
321
+ const stdinTarget = needsStdin && !useWindowsStdinFile ? 'pipe' : 'ignore';
160
322
  const stdio = [stdinTarget, 'ignore', 'ignore'];
161
323
  // pln#476 + pln#520 step 4: wrap the spawn so the worker shell touches the
162
324
  // pre-exec `ack` sentinel, redirects logs at the shell level, and emits a
@@ -168,12 +330,25 @@ export class CliExecutionAdapter {
168
330
  if (useAckWrap) {
169
331
  const signalRoot = options.ackRoot ?? options.worktreePath;
170
332
  ensureRuntimeDirs(signalRoot);
333
+ const runtimeRunId = options.turnEcho?.run_id;
334
+ const ackPath = getRuntimeSignalPath(signalRoot, options.assignmentId, 'ack', runtimeRunId);
335
+ const contractBootstrapPath = `${ackPath}.bootstrap.cjs`;
336
+ if (options.turnEcho?.contract_hash && options.turnEcho.capability_snapshot_hash) {
337
+ writeContractBootstrapScript(contractBootstrapPath);
338
+ }
339
+ const stdinFilePath = useWindowsStdinFile ? `${ackPath}.stdin` : undefined;
340
+ if (stdinFilePath) {
341
+ fs.writeFileSync(stdinFilePath, invoke.promptText, { encoding: 'utf8', mode: 0o600 });
342
+ }
171
343
  const wrappedCmd = buildAckWrapCommand(invoke.bashCommand, {
172
- ackPath: getRuntimeSignalPath(signalRoot, options.assignmentId, 'ack'),
173
- completedPath: getRuntimeSignalPath(signalRoot, options.assignmentId, 'completed'),
174
- failedPath: getRuntimeSignalPath(signalRoot, options.assignmentId, 'failed'),
175
- stdoutLog: getRuntimeLogPath(signalRoot, options.assignmentId, 'stdout'),
176
- stderrLog: getRuntimeLogPath(signalRoot, options.assignmentId, 'stderr'),
344
+ ackPath,
345
+ completedPath: getRuntimeSignalPath(signalRoot, options.assignmentId, 'completed', runtimeRunId),
346
+ failedPath: getRuntimeSignalPath(signalRoot, options.assignmentId, 'failed', runtimeRunId),
347
+ stdoutLog: getRuntimeLogPath(signalRoot, options.assignmentId, 'stdout', runtimeRunId),
348
+ stderrLog: getRuntimeLogPath(signalRoot, options.assignmentId, 'stderr', runtimeRunId),
349
+ stdinFilePath,
350
+ contractBootstrapPath,
351
+ expectedWorkspacePath: options.worktreePath,
177
352
  }, isWin32, options.turnEcho);
178
353
  child = spawn(wrappedCmd, [], {
179
354
  detached: !isWin32,
@@ -201,7 +376,7 @@ export class CliExecutionAdapter {
201
376
  // On Windows shell:true: this never fires for ENOENT (cmd.exe succeeds);
202
377
  // the isBinaryOnPath pre-check above catches that case instead.
203
378
  child.on('error', () => { });
204
- if (needsStdin && child.stdin) {
379
+ if (needsStdin && !useWindowsStdinFile && child.stdin) {
205
380
  child.stdin.write(invoke.promptText);
206
381
  child.stdin.end();
207
382
  }
@@ -0,0 +1,345 @@
1
+ import crypto from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { getCapabilityProfile, dispatchCanCommit, } from './agent-capability.js';
4
+ import { ExpectedArtifactSchema } from './loops/artifact-contract.js';
5
+ export const EXECUTION_CONTRACT_VERSION = 1;
6
+ export const EXECUTION_CONTRACT_PROTOCOL_VERSION = 1;
7
+ const RoleCapabilitySchema = z.enum(['execute', 'coordinate', 'review', 'consult']);
8
+ const ExecutionSurfaceSchema = z.enum(['cli', 'ide', 'extension', 'remote']);
9
+ export const CapabilityRequirementSchema = z.object({
10
+ roles: z.array(RoleCapabilitySchema).default(['execute']),
11
+ required_surfaces: z.array(z.enum([
12
+ 'mcp',
13
+ 'hooks',
14
+ 'skills',
15
+ 'rules',
16
+ 'auto_approve',
17
+ 'cli_spawn',
18
+ 'commit',
19
+ 'inbox',
20
+ ])).default([]),
21
+ execution_surfaces: z.array(ExecutionSurfaceSchema).default([]),
22
+ model: z.string().min(1).optional(),
23
+ required_tools: z.array(z.string().min(1)).default([]),
24
+ });
25
+ export const CapabilityResolutionReasonSchema = z.object({
26
+ code: z.enum([
27
+ 'agent_profile_missing',
28
+ 'role_unsupported',
29
+ 'surface_unsupported',
30
+ 'execution_surface_mismatch',
31
+ 'model_unsupported',
32
+ 'tool_catalog_unattested',
33
+ ]),
34
+ requirement: z.string().min(1),
35
+ expected: z.string().optional(),
36
+ actual: z.string().optional(),
37
+ });
38
+ export const HarnessCapabilityBindingSchema = z.object({
39
+ adapter_id: z.string().min(1),
40
+ adapter_version: z.string().min(1),
41
+ requested_model: z.string().min(1).optional(),
42
+ resolved_model: z.string().min(1).optional(),
43
+ model_resolution: z.enum(['exact', 'defaulted', 'unattested']),
44
+ });
45
+ export const CapabilitySnapshotSchema = z.object({
46
+ schema_version: z.literal(1),
47
+ agent: z.string().min(1),
48
+ agent_id: z.string().min(1).optional(),
49
+ profile_name: z.string().min(1).optional(),
50
+ accepted: z.boolean(),
51
+ requested: CapabilityRequirementSchema,
52
+ resolved: z.object({
53
+ roles: z.array(RoleCapabilitySchema),
54
+ surfaces: z.array(z.string().min(1)),
55
+ execution_surface: ExecutionSurfaceSchema.optional(),
56
+ model: z.string().min(1).optional(),
57
+ invoke_binary: z.string().min(1).optional(),
58
+ tool_catalog_attested: z.boolean(),
59
+ harness: HarnessCapabilityBindingSchema.optional(),
60
+ }),
61
+ reasons: z.array(CapabilityResolutionReasonSchema),
62
+ });
63
+ export const ExecutionContractSchema = z.object({
64
+ schema_version: z.literal(EXECUTION_CONTRACT_VERSION),
65
+ minimum_reader_version: z.number().int().positive().max(EXECUTION_CONTRACT_VERSION).default(1),
66
+ identity: z.object({
67
+ loop_id: z.string().min(1),
68
+ turn_id: z.string().min(1),
69
+ logical_attempt_epoch: z.number().int().nonnegative(),
70
+ assignment_id: z.string().min(1),
71
+ run_id: z.string().min(1),
72
+ kind: z.enum(['review', 'ideation', 'implementation', 'research', 'debug']),
73
+ phase: z.string().min(1),
74
+ iteration: z.number().int().nonnegative(),
75
+ }),
76
+ artifact_contract: z.object({
77
+ completion_mode: z.enum(['file', 'mcp', 'either']),
78
+ expected_artifacts: z.array(ExpectedArtifactSchema),
79
+ }),
80
+ capability_requirement: CapabilityRequirementSchema,
81
+ workspace_policy: z.object({
82
+ scope: z.string().min(1),
83
+ cwd: z.string().min(1),
84
+ worktree_path: z.string().min(1).optional(),
85
+ isolation: z.enum(['worktree', 'shared_checkout', 'none']),
86
+ write_access: z.enum(['read_only', 'workspace', 'unrestricted']),
87
+ }),
88
+ timeout_policy: z.object({
89
+ dispatch_lease_ms: z.number().int().positive(),
90
+ grant_lease_ms: z.number().int().positive(),
91
+ }),
92
+ evidence_policy: z.object({
93
+ require_turn_id: z.literal(true),
94
+ require_run_id: z.literal(true),
95
+ require_nonce: z.literal(true),
96
+ artifact_hash: z.enum(['required', 'optional']),
97
+ }),
98
+ protocol: z.object({
99
+ name: z.literal('attempt-authority'),
100
+ minimum_version: z.number().int().positive().max(EXECUTION_CONTRACT_PROTOCOL_VERSION),
101
+ }),
102
+ });
103
+ export const ExecutionContractRefSchema = z.object({
104
+ version: z.literal(EXECUTION_CONTRACT_VERSION),
105
+ hash: z.string().regex(/^[a-f0-9]{64}$/),
106
+ snapshot_hash: z.string().regex(/^[a-f0-9]{64}$/),
107
+ turn_id: z.string().min(1),
108
+ });
109
+ export const RuntimeCapabilityObservationSchema = z.object({
110
+ contract_hash: z.string().regex(/^[a-f0-9]{64}$/),
111
+ capability_snapshot_hash: z.string().regex(/^[a-f0-9]{64}$/),
112
+ adapter_id: z.string().min(1).optional(),
113
+ adapter_version: z.string().min(1).optional(),
114
+ observed_surfaces: z.array(z.string().min(1)).default([]),
115
+ observed_model: z.string().min(1).optional(),
116
+ accepted_contract_hash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
117
+ accepted_capability_snapshot_hash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
118
+ });
119
+ function canonicalValue(value) {
120
+ if (typeof value === 'string')
121
+ return value.normalize('NFC');
122
+ if (Array.isArray(value))
123
+ return value.map(canonicalValue);
124
+ if (value && typeof value === 'object') {
125
+ const normalized = Object.entries(value)
126
+ .filter(([, item]) => item !== undefined)
127
+ .map(([key, item]) => [key.normalize('NFC'), canonicalValue(item)])
128
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
129
+ if (new Set(normalized.map(([key]) => key)).size !== normalized.length) {
130
+ throw new Error('canonical execution contract contains duplicate NFC-normalized keys');
131
+ }
132
+ return Object.fromEntries(normalized);
133
+ }
134
+ return value;
135
+ }
136
+ export function canonicalExecutionContract(contract) {
137
+ return JSON.stringify(canonicalValue(ExecutionContractSchema.parse(contract)));
138
+ }
139
+ export function executionContractHash(contract) {
140
+ return crypto.createHash('sha256').update(canonicalExecutionContract(contract), 'utf8').digest('hex');
141
+ }
142
+ export function capabilitySnapshotHash(snapshot) {
143
+ const canonical = JSON.stringify(canonicalValue(CapabilitySnapshotSchema.parse(snapshot)));
144
+ return crypto.createHash('sha256').update(canonical, 'utf8').digest('hex');
145
+ }
146
+ function profileSurfaces(profile) {
147
+ const surfaces = [
148
+ profile.hasMcp && 'mcp',
149
+ profile.hasHooks && 'hooks',
150
+ profile.hasSkills && 'skills',
151
+ profile.hasRules && 'rules',
152
+ profile.hasAutoApprove && 'auto_approve',
153
+ profile.runtime.canBeSpawnedCli && 'cli_spawn',
154
+ profile.runtime.inbox && 'inbox',
155
+ dispatchCanCommit(profile) && 'commit',
156
+ ].filter((value) => Boolean(value));
157
+ return [...new Set(surfaces)].sort();
158
+ }
159
+ function resolvedModel(profile, requested) {
160
+ if (!requested)
161
+ return profile.default_model;
162
+ if (profile.model_flag || profile.default_model === requested)
163
+ return requested;
164
+ return undefined;
165
+ }
166
+ export function resolveCapabilitySnapshot(agent, requirementInput, agentId, harness) {
167
+ const requested = CapabilityRequirementSchema.parse(requirementInput);
168
+ const harnessBinding = harness ? HarnessCapabilityBindingSchema.parse(harness) : undefined;
169
+ const profile = getCapabilityProfile(agent);
170
+ const reasons = [];
171
+ if (!profile) {
172
+ reasons.push({ code: 'agent_profile_missing', requirement: 'agent_profile', expected: agent });
173
+ return CapabilitySnapshotSchema.parse({
174
+ schema_version: 1,
175
+ agent,
176
+ agent_id: agentId,
177
+ accepted: false,
178
+ requested,
179
+ resolved: { roles: [], surfaces: [], tool_catalog_attested: false },
180
+ reasons,
181
+ });
182
+ }
183
+ const surfaces = profileSurfaces(profile);
184
+ for (const role of requested.roles) {
185
+ if (!profile.role_capabilities.includes(role)) {
186
+ reasons.push({
187
+ code: 'role_unsupported',
188
+ requirement: `role:${role}`,
189
+ expected: role,
190
+ actual: profile.role_capabilities.join(','),
191
+ });
192
+ }
193
+ }
194
+ for (const surface of requested.required_surfaces) {
195
+ if (!surfaces.includes(surface)) {
196
+ reasons.push({
197
+ code: 'surface_unsupported',
198
+ requirement: `surface:${surface}`,
199
+ expected: surface,
200
+ actual: surfaces.join(','),
201
+ });
202
+ }
203
+ }
204
+ if (requested.execution_surfaces.length > 0
205
+ && !requested.execution_surfaces.includes(profile.execution_env.surface)) {
206
+ reasons.push({
207
+ code: 'execution_surface_mismatch',
208
+ requirement: 'execution_surface',
209
+ expected: requested.execution_surfaces.join(','),
210
+ actual: profile.execution_env.surface,
211
+ });
212
+ }
213
+ const model = resolvedModel(profile, requested.model);
214
+ if (requested.model && !model) {
215
+ reasons.push({
216
+ code: 'model_unsupported',
217
+ requirement: 'model',
218
+ expected: requested.model,
219
+ actual: profile.default_model,
220
+ });
221
+ }
222
+ // Current profiles attest transport surfaces, not per-tool catalogs. Refuse
223
+ // named tools rather than guessing from hasMcp/hasSkills.
224
+ if (requested.required_tools.length > 0) {
225
+ reasons.push({
226
+ code: 'tool_catalog_unattested',
227
+ requirement: 'required_tools',
228
+ expected: requested.required_tools.join(','),
229
+ actual: 'unattested',
230
+ });
231
+ }
232
+ return CapabilitySnapshotSchema.parse({
233
+ schema_version: 1,
234
+ agent,
235
+ agent_id: agentId,
236
+ profile_name: profile.name,
237
+ accepted: reasons.length === 0,
238
+ requested,
239
+ resolved: {
240
+ roles: profile.role_capabilities,
241
+ surfaces,
242
+ execution_surface: profile.execution_env.surface,
243
+ model,
244
+ invoke_binary: profile.invoke_binary,
245
+ tool_catalog_attested: false,
246
+ harness: harnessBinding,
247
+ },
248
+ reasons,
249
+ });
250
+ }
251
+ export function executionContractRef(contract, snapshot) {
252
+ return {
253
+ version: contract.schema_version,
254
+ hash: executionContractHash(contract),
255
+ snapshot_hash: capabilitySnapshotHash(snapshot),
256
+ turn_id: contract.identity.turn_id,
257
+ };
258
+ }
259
+ export function assertExecutionContractIntegrity(contract, ref, snapshot, selectedAgent) {
260
+ const parsedContract = ExecutionContractSchema.parse(contract);
261
+ const parsedRef = ExecutionContractRefSchema.parse(ref);
262
+ const parsedSnapshot = CapabilitySnapshotSchema.parse(snapshot);
263
+ if (parsedRef.turn_id !== parsedContract.identity.turn_id) {
264
+ throw new Error(`execution contract turn mismatch: ${parsedRef.turn_id} != ${parsedContract.identity.turn_id}`);
265
+ }
266
+ const actualHash = executionContractHash(parsedContract);
267
+ if (parsedRef.hash !== actualHash) {
268
+ throw new Error(`execution contract hash mismatch: ${parsedRef.hash} != ${actualHash}`);
269
+ }
270
+ const actualSnapshotHash = capabilitySnapshotHash(parsedSnapshot);
271
+ if (parsedRef.snapshot_hash !== actualSnapshotHash) {
272
+ throw new Error(`capability snapshot hash mismatch: ${parsedRef.snapshot_hash} != ${actualSnapshotHash}`);
273
+ }
274
+ if (!parsedSnapshot.accepted) {
275
+ throw new Error('execution contract capability snapshot was not accepted');
276
+ }
277
+ if (JSON.stringify(parsedSnapshot.requested) !== JSON.stringify(parsedContract.capability_requirement)) {
278
+ throw new Error('execution contract capability requirement differs from its resolved snapshot');
279
+ }
280
+ if (selectedAgent && parsedSnapshot.agent !== selectedAgent.agent) {
281
+ throw new Error(`capability snapshot agent mismatch: ${parsedSnapshot.agent} != ${selectedAgent.agent}`);
282
+ }
283
+ if (selectedAgent && parsedSnapshot.agent_id !== selectedAgent.agent_id) {
284
+ throw new Error(`capability snapshot agent_id mismatch: ${parsedSnapshot.agent_id ?? 'none'} != ${selectedAgent.agent_id ?? 'none'}`);
285
+ }
286
+ }
287
+ /**
288
+ * Pre-crossing acceptance emitted by the frozen harness adapter. The child
289
+ * process still confirms the effective environment in its bootstrap ACK after
290
+ * spawn; this attestation proves, before authority crosses, that the selected
291
+ * adapter accepted the exact immutable contract it is about to deliver.
292
+ */
293
+ export function attestHarnessContractAcceptance(expectedRef, snapshot, binding) {
294
+ const parsedSnapshot = CapabilitySnapshotSchema.parse(snapshot);
295
+ const parsedBinding = HarnessCapabilityBindingSchema.parse(binding);
296
+ if (!parsedSnapshot.accepted) {
297
+ throw new Error('harness cannot accept a rejected capability snapshot');
298
+ }
299
+ const frozen = parsedSnapshot.resolved.harness;
300
+ if (!frozen)
301
+ throw new Error('capability snapshot has no frozen harness binding');
302
+ if (frozen.adapter_id !== parsedBinding.adapter_id
303
+ || frozen.adapter_version !== parsedBinding.adapter_version
304
+ || frozen.requested_model !== parsedBinding.requested_model
305
+ || frozen.resolved_model !== parsedBinding.resolved_model) {
306
+ throw new Error(`harness acceptance mismatch: frozen ${frozen.adapter_id}@${frozen.adapter_version}, `
307
+ + `selected ${parsedBinding.adapter_id}@${parsedBinding.adapter_version}`);
308
+ }
309
+ return {
310
+ contract_hash: expectedRef.hash,
311
+ capability_snapshot_hash: expectedRef.snapshot_hash,
312
+ };
313
+ }
314
+ /**
315
+ * Deterministic capability selection/reselection. Input order is deliberately
316
+ * irrelevant so replaying the same candidate set produces the same worker.
317
+ * Callers can exclude a failed pre-cross candidate and run the resolver again.
318
+ */
319
+ export function resolveExecutionCandidate(candidates, requirementInput, options = {}) {
320
+ const requirement = CapabilityRequirementSchema.parse(requirementInput);
321
+ const excluded = new Set((options.exclude ?? []).map(({ agent, agent_id }) => `${agent.normalize('NFC')}\0${(agent_id ?? '').normalize('NFC')}`));
322
+ const ordered = candidates
323
+ .filter(({ agent, agent_id }) => !excluded.has(`${agent.normalize('NFC')}\0${(agent_id ?? '').normalize('NFC')}`))
324
+ .map((candidate) => ({ ...candidate, agent: candidate.agent.normalize('NFC'), agent_id: candidate.agent_id?.normalize('NFC') }))
325
+ .sort((left, right) => (right.preference ?? 0) - (left.preference ?? 0)
326
+ || left.agent.localeCompare(right.agent, 'en')
327
+ || (left.agent_id ?? '').localeCompare(right.agent_id ?? '', 'en'));
328
+ const evaluated = ordered.map((candidate) => ({
329
+ ...candidate,
330
+ snapshot: resolveCapabilitySnapshot(candidate.agent, requirement, candidate.agent_id),
331
+ }));
332
+ const selected = evaluated.find((candidate) => candidate.snapshot.accepted);
333
+ return selected ? { kind: 'selected', selected, evaluated } : { kind: 'rejected', evaluated };
334
+ }
335
+ export function validateWorkerContractAcceptance(expectedRef, acceptedRef, launchStatus) {
336
+ const expected = { contract_hash: expectedRef.hash, capability_snapshot_hash: expectedRef.snapshot_hash };
337
+ if (expected.contract_hash === acceptedRef.contract_hash
338
+ && expected.capability_snapshot_hash === acceptedRef.capability_snapshot_hash)
339
+ return { kind: 'accepted' };
340
+ if (launchStatus === 'crossed') {
341
+ return { kind: 'post_crossing_anomaly', expected, accepted: acceptedRef, respawn: false };
342
+ }
343
+ return { kind: 'abort_and_reselect', expected, accepted: acceptedRef };
344
+ }
345
+ //# sourceMappingURL=execution-contract.js.map