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
@@ -26,8 +26,11 @@ import { spawnSync } from 'node:child_process';
26
26
  import path from 'node:path';
27
27
  import { getLoop } from './store.js';
28
28
  import { withLoopLock } from './lock.js';
29
- import { add_artifact } from './verbs.js';
29
+ import { addArtifactWithEvidence } from './verbs.js';
30
30
  import { artifactsInIteration } from './iteration-engine.js';
31
+ import { evidenceDigest } from './evidence.js';
32
+ import { eligibleArtifactsForPurpose } from './gate-policy.js';
33
+ import { captureWorkspaceDigest } from './workspace-digest.js';
31
34
  import { VERIFY_DEFAULT_TIMEOUT_MS, LOOP_ARTIFACT_BODY_MAX_BYTES, } from './types.js';
32
35
  /** VerifyReportBodySchema caps stdout_tail/stderr_tail at 1024. */
33
36
  const TAIL_MAX = 1024;
@@ -115,9 +118,10 @@ function fitBody(report) {
115
118
  }
116
119
  return { ...report, stdout_tail: undefined, stderr_tail: undefined };
117
120
  }
118
- export function buildVerifyReportBody(config, result) {
121
+ export function buildVerifyReportBody(config, result, bindings) {
119
122
  return fitBody({
120
123
  command: config.command.join(' '),
124
+ command_argv: config.command,
121
125
  exit_code: result.exit_code,
122
126
  passed: result.passed,
123
127
  duration_ms: result.duration_ms,
@@ -125,11 +129,13 @@ export function buildVerifyReportBody(config, result) {
125
129
  timed_out: result.timed_out,
126
130
  stdout_tail: result.stdout_tail || undefined,
127
131
  stderr_tail: result.stderr_tail || undefined,
132
+ ...bindings,
128
133
  });
129
134
  }
130
- /** True when a verify_report already exists in this iteration (idempotency key = loop+iteration). */
135
+ /** True when an authoritative, still-fresh engine report exists for this iteration. */
131
136
  function hasVerifyReportForIteration(thread, iteration) {
132
- return artifactsInIteration(thread, iteration).some((a) => a.type === 'verify_report');
137
+ const reports = artifactsInIteration(thread, iteration).filter((artifact) => artifact.type === 'verify_report');
138
+ return eligibleArtifactsForPurpose(thread, reports, 'command_green').eligible.length > 0;
133
139
  }
134
140
  /**
135
141
  * Run the configured verify command and record a deterministic `verify_report` for the
@@ -170,7 +176,12 @@ export function runVerify(input, cwd) {
170
176
  return { thread: snapshot.thread, deduped: true };
171
177
  // --- OUT OF LOCK: run the command (may take minutes). ---
172
178
  const { config, iteration, phase } = snapshot;
173
- const report = buildVerifyReportBody(config, runner(config));
179
+ const command_digest = evidenceDigest({ command: config.command });
180
+ const workspaceBefore = captureWorkspaceDigest(config.cwd);
181
+ const runResult = runner(config);
182
+ const workspaceAfter = captureWorkspaceDigest(config.cwd);
183
+ const workspace_stable = workspaceBefore === workspaceAfter;
184
+ const reportAfterRun = buildVerifyReportBody(config, { ...runResult, passed: runResult.passed && workspace_stable }, { command_digest, workspace_digest: workspaceAfter, workspace_stable });
174
185
  // --- Lock scope 2: re-check idempotency (by SNAPSHOT iteration), then append. ---
175
186
  return withLoopLock({
176
187
  cwd,
@@ -185,11 +196,24 @@ export function runVerify(input, cwd) {
185
196
  // landed (a concurrent verify won). Checking the snapshot (not the current)
186
197
  // iteration is what makes this correct after a concurrent advance (review F1).
187
198
  if (hasVerifyReportForIteration(thread, iteration)) {
188
- return { thread, report, deduped: true };
199
+ return { thread, report: reportAfterRun, deduped: true };
189
200
  }
190
- const updated = add_artifact({
201
+ // Close the final out-of-lock race: the bytes verified above must still be the
202
+ // bytes present at the evidence commit boundary. A later gate independently
203
+ // repeats this freshness check so a post-commit mutation also fails closed.
204
+ const workspaceAtCommit = captureWorkspaceDigest(config.cwd);
205
+ const commitStable = workspace_stable && workspaceAtCommit === workspaceAfter;
206
+ const report = buildVerifyReportBody(config, { ...runResult, passed: runResult.passed && commitStable }, { command_digest, workspace_digest: workspaceAtCommit, workspace_stable: commitStable });
207
+ const updated = addArtifactWithEvidence({
191
208
  id: input.loop_id,
192
209
  actor: input.actor,
210
+ evidence_context: {
211
+ channel: 'verify_command',
212
+ producer_kind: 'engine',
213
+ producer_id: 'brainclaw:verify-command',
214
+ command_digest,
215
+ workspace_digest: workspaceAtCommit,
216
+ },
193
217
  artifact: {
194
218
  // Stamp the SNAPSHOT phase + iteration so the report is attributed to the
195
219
  // iteration whose code it actually tested — never a later iteration a
@@ -198,7 +222,6 @@ export function runVerify(input, cwd) {
198
222
  iteration,
199
223
  type: 'verify_report',
200
224
  body: JSON.stringify(report),
201
- produced_by: 'engine',
202
225
  },
203
226
  }, cwd);
204
227
  const art = updated.artifacts[updated.artifacts.length - 1];
@@ -0,0 +1,54 @@
1
+ import crypto from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ function fallbackFiles(root, current = root) {
6
+ const files = [];
7
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
8
+ if (entry.name === '.brainclaw' || entry.name === '.git' || entry.name === 'node_modules')
9
+ continue;
10
+ const absolute = path.join(current, entry.name);
11
+ if (entry.isDirectory())
12
+ files.push(...fallbackFiles(root, absolute));
13
+ else if (entry.isFile())
14
+ files.push(path.relative(root, absolute));
15
+ }
16
+ return files;
17
+ }
18
+ /** Hash the exact tracked/untracked workspace bytes at a gate boundary. */
19
+ export function captureWorkspaceDigest(cwd) {
20
+ const rootResult = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
21
+ encoding: 'utf8', shell: false, maxBuffer: 1024 * 1024,
22
+ });
23
+ const gitRoot = rootResult.status === 0 ? rootResult.stdout.trim() : undefined;
24
+ let files;
25
+ let root;
26
+ if (gitRoot) {
27
+ root = path.resolve(gitRoot);
28
+ const listed = spawnSync('git', ['-C', root, 'ls-files', '--cached', '--others', '--exclude-standard', '-z'], {
29
+ encoding: 'buffer', shell: false, maxBuffer: 32 * 1024 * 1024,
30
+ });
31
+ files = listed.status === 0
32
+ ? listed.stdout.toString('utf8').split('\0').filter(Boolean)
33
+ : fallbackFiles(root);
34
+ }
35
+ else {
36
+ root = path.resolve(cwd);
37
+ files = fallbackFiles(root);
38
+ }
39
+ const hash = crypto.createHash('sha256').update(root.normalize('NFC'));
40
+ for (const relative of [...new Set(files)].sort()) {
41
+ const absolute = path.resolve(root, relative);
42
+ try {
43
+ const stat = fs.lstatSync(absolute);
44
+ if (!stat.isFile())
45
+ continue;
46
+ hash.update('\0').update(relative.normalize('NFC')).update('\0').update(fs.readFileSync(absolute));
47
+ }
48
+ catch {
49
+ hash.update('\0missing\0').update(relative.normalize('NFC'));
50
+ }
51
+ }
52
+ return hash.digest('hex');
53
+ }
54
+ //# sourceMappingURL=workspace-digest.js.map
@@ -1,5 +1,5 @@
1
1
  import { getLoop } from './loops/store.js';
2
- import { complete_turn, advance } from './loops/verbs.js';
2
+ import { completeTurnWithEvidence, advance } from './loops/verbs.js';
3
3
  import { withLoopLock } from './loops/lock.js';
4
4
  import { LOOP_ARTIFACT_BODY_MAX_BYTES } from './loops/types.js';
5
5
  /** review-loop:lop_xxx → the loop id (mirrors assignment-reconciler.ts). */
@@ -93,8 +93,19 @@ export function closeReviewLoopFromLaneResult(assignment, lane, actor, cwd, opti
93
93
  if (verdict === 'approve') {
94
94
  if (slot) {
95
95
  // isVerdictAccepted fires reviewer_green ONLY on an "accepted…" body.
96
- complete_turn({
96
+ completeTurnWithEvidence({
97
97
  id: loopId, slot_id: slot.slot_id, actor,
98
+ evidence_context: {
99
+ channel: 'complete_turn',
100
+ producer_kind: 'slot',
101
+ producer_id: slot.slot_id,
102
+ agent_id: slot.agent_id,
103
+ slot_id: slot.slot_id,
104
+ slot_role: slot.role,
105
+ turn_id: slot.current_turn_id,
106
+ assignment_id: slot.assignment_id ?? assignment.id,
107
+ claim_id: slot.claim_id,
108
+ },
98
109
  // pln#639 BUG-2 — the phase the slot was DISPATCHED in, not the
99
110
  // loop's phase at close time. Same defect as the ideation closer;
100
111
  // fixed here too because this is the far more travelled path.
@@ -145,8 +156,19 @@ export function closeReviewLoopFromLaneResult(assignment, lane, actor, cwd, opti
145
156
  // is a planned follow-up, so a human drives it. (No re-dispatch means no
146
157
  // worktree reuse, so the claim is released by harvest as usual.)
147
158
  const symmetric = loop.protocol?.review_mode === 'symmetric';
148
- complete_turn({
159
+ completeTurnWithEvidence({
149
160
  id: loopId, slot_id: slot.slot_id, actor,
161
+ evidence_context: {
162
+ channel: 'complete_turn',
163
+ producer_kind: 'slot',
164
+ producer_id: slot.slot_id,
165
+ agent_id: slot.agent_id,
166
+ slot_id: slot.slot_id,
167
+ slot_role: slot.role,
168
+ turn_id: slot.current_turn_id,
169
+ assignment_id: slot.assignment_id ?? assignment.id,
170
+ claim_id: slot.claim_id,
171
+ },
150
172
  // pln#639 BUG-2 — dispatch phase, not close-time phase (see above).
151
173
  artifact: { phase: slot.phase ?? loop.current_phase, type: 'verdict', body: capVerdictBody('changes-requested', detail) },
152
174
  }, cwd);