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
@@ -0,0 +1,533 @@
1
+ import { evidenceDigest, validateArtifactEvidence } from './evidence.js';
2
+ import { captureWorkspaceDigest } from './workspace-digest.js';
3
+ import { currentNonce, findReservationByAssignmentId, getReservation, } from './attempt-reservation.js';
4
+ import { readLaunchDecision, resolveTurnGenerationChain, } from './attempt-generations.js';
5
+ const OBSERVATION = {
6
+ right: 'gate:artifact',
7
+ authorities: [
8
+ { channel: 'complete_turn', producer_kind: 'slot', attestation: 'observation', issuer: 'brainclaw:artifact-commit', required_subject_fields: ['slot_id'] },
9
+ { channel: 'reconcile_turn', producer_kind: 'slot', attestation: 'observation', issuer: 'brainclaw:artifact-commit', required_subject_fields: ['slot_id', 'turn_id', 'run_id', 'nonce_digest', 'attempt_epoch', 'execution_contract_hash', 'workspace_digest'] },
10
+ { channel: 'operator_input', producer_kind: 'slot', attestation: 'observation', issuer: 'brainclaw:artifact-commit', required_subject_fields: ['slot_id'] },
11
+ { channel: 'operator_input', producer_kind: 'operator', attestation: 'observation', issuer: 'brainclaw:artifact-commit' },
12
+ { channel: 'operator_input', producer_kind: 'engine', attestation: 'observation', issuer: 'brainclaw:artifact-commit' },
13
+ { channel: 'system_hook', producer_kind: 'engine', attestation: 'observation', issuer: 'brainclaw:artifact-commit' },
14
+ { channel: 'verify_command', producer_kind: 'engine', attestation: 'verification', issuer: 'brainclaw:verify-command', required_subject_fields: ['command_digest', 'workspace_digest'] },
15
+ ],
16
+ };
17
+ const APPROVAL = {
18
+ right: 'gate:reviewer_green',
19
+ authorities: [
20
+ { channel: 'complete_turn', producer_kind: 'slot', attestation: 'approval', issuer: 'brainclaw:review-slot', required_subject_fields: ['slot_id'] },
21
+ { channel: 'reconcile_turn', producer_kind: 'slot', attestation: 'approval', issuer: 'brainclaw:review-slot', required_subject_fields: ['slot_id', 'turn_id', 'run_id', 'nonce_digest', 'attempt_epoch', 'execution_contract_hash', 'workspace_digest'] },
22
+ ],
23
+ };
24
+ const VERIFICATION = {
25
+ right: 'gate:command_green',
26
+ authorities: [
27
+ { channel: 'verify_command', producer_kind: 'engine', attestation: 'verification', issuer: 'brainclaw:verify-command', required_subject_fields: ['command_digest', 'workspace_digest'] },
28
+ ],
29
+ };
30
+ const CRITIC_OBSERVATION = {
31
+ right: 'gate:artifact',
32
+ authorities: OBSERVATION.authorities.filter((authority) => authority.channel === 'complete_turn' || authority.channel === 'reconcile_turn'),
33
+ };
34
+ const NO_REVIEWER_GREEN = { right: 'gate:reviewer_green', authorities: [] };
35
+ const NO_COMMAND_GREEN = { right: 'gate:command_green', authorities: [] };
36
+ const NO_CRITIC_SIGNAL = { right: 'gate:artifact', authorities: [] };
37
+ /** Policies refine the five protocol graphs; they do not duplicate phase order. */
38
+ export const GATE_POLICIES = {
39
+ review: {
40
+ version: 'gate-policy-v1',
41
+ kind: 'review',
42
+ requirements: {
43
+ artifact: OBSERVATION,
44
+ reviewer_green: APPROVAL,
45
+ command_green: NO_COMMAND_GREEN,
46
+ critic_signal: NO_CRITIC_SIGNAL,
47
+ },
48
+ },
49
+ ideation: {
50
+ version: 'gate-policy-v1',
51
+ kind: 'ideation',
52
+ requirements: {
53
+ artifact: OBSERVATION,
54
+ reviewer_green: NO_REVIEWER_GREEN,
55
+ command_green: NO_COMMAND_GREEN,
56
+ critic_signal: CRITIC_OBSERVATION,
57
+ },
58
+ },
59
+ implementation: {
60
+ version: 'gate-policy-v1',
61
+ kind: 'implementation',
62
+ requirements: {
63
+ artifact: OBSERVATION,
64
+ reviewer_green: NO_REVIEWER_GREEN,
65
+ command_green: VERIFICATION,
66
+ critic_signal: NO_CRITIC_SIGNAL,
67
+ },
68
+ },
69
+ research: {
70
+ version: 'gate-policy-v1',
71
+ kind: 'research',
72
+ requirements: {
73
+ artifact: OBSERVATION,
74
+ reviewer_green: NO_REVIEWER_GREEN,
75
+ command_green: NO_COMMAND_GREEN,
76
+ // The shipped research FSM exits investigate↔synthesize on this signal.
77
+ critic_signal: CRITIC_OBSERVATION,
78
+ },
79
+ },
80
+ debug: {
81
+ version: 'gate-policy-v1',
82
+ kind: 'debug',
83
+ requirements: {
84
+ artifact: OBSERVATION,
85
+ reviewer_green: NO_REVIEWER_GREEN,
86
+ // The shipped debug FSM exits hypothesize↔isolate↔fix on command_green.
87
+ command_green: VERIFICATION,
88
+ critic_signal: NO_CRITIC_SIGNAL,
89
+ },
90
+ },
91
+ };
92
+ function hasUsableContent(artifact) {
93
+ return (artifact.body ?? '').trim().length > 0 || artifact.ref !== undefined;
94
+ }
95
+ function legacyEligibleCount(thread, artifacts, purpose, cwd) {
96
+ return legacyEligibleArtifacts(thread, artifacts, purpose, cwd).length;
97
+ }
98
+ function legacyEligibleArtifacts(thread, artifacts, purpose, cwd) {
99
+ // Kind-specialized purposes stay fail-closed even for persisted legacy
100
+ // loops. Legacy relaxes envelope presence; it does not invent an authority
101
+ // that the kind's policy explicitly forbids.
102
+ if (GATE_POLICIES[thread.kind].requirements[purpose].authorities.length === 0)
103
+ return [];
104
+ return artifacts.filter((artifact) => {
105
+ if (purpose !== 'critic_signal' && !hasUsableContent(artifact))
106
+ return false;
107
+ if (!artifact.evidence)
108
+ return true;
109
+ return validateArtifactEvidence(thread, artifact).valid
110
+ && reconciledV2AuthorityRejection(thread, artifact, cwd) === undefined;
111
+ });
112
+ }
113
+ function payloadFingerprint(artifact) {
114
+ return evidenceDigest({
115
+ phase: artifact.phase,
116
+ type: artifact.type,
117
+ body: artifact.body,
118
+ ref: artifact.ref,
119
+ iteration: artifact.iteration ?? 0,
120
+ producer: artifact.evidence?.subject.slot_id ?? artifact.evidence?.producer.id ?? artifact.produced_by,
121
+ });
122
+ }
123
+ /**
124
+ * A reconciled v2 envelope is only a claim about an attempt until the gate
125
+ * compares it with immutable AttemptAuthority. The persisted nonce is a digest
126
+ * (never the bearer token), so compare it with a server-derived digest of the
127
+ * current generation nonce.
128
+ *
129
+ * No v2 chain enters an explicit restrictive legacy verifier backed by the
130
+ * committed reservation. Missing/unverifiable authority and every tuple
131
+ * mismatch are fail-closed in both modes.
132
+ */
133
+ function reconciledV2AuthorityRejection(thread, artifact, cwd) {
134
+ const envelope = artifact.evidence;
135
+ if (!envelope || envelope.producer.channel !== 'reconcile_turn')
136
+ return undefined;
137
+ const subject = envelope.subject;
138
+ const reservation = subject.assignment_id
139
+ ? findReservationByAssignmentId(subject.assignment_id, cwd)
140
+ : undefined;
141
+ const resolvedReservation = reservation ?? (subject.turn_id ? getReservation(subject.turn_id, cwd) : undefined);
142
+ if (!resolvedReservation)
143
+ return 'reconciled_authority_missing';
144
+ let chain;
145
+ try {
146
+ chain = resolveTurnGenerationChain(resolvedReservation.store_root, resolvedReservation.turn_id);
147
+ }
148
+ catch {
149
+ return 'reconciled_authority_unreadable';
150
+ }
151
+ if (!chain) {
152
+ // Legacy is deliberately narrow, not permissive: every binding is
153
+ // reconstructed from the committed, crossed reservation. Pre-P1 records
154
+ // have no stored ExecutionContract, so use the same deterministic
155
+ // compatibility identities the reconciler seals. This preserves dual-read
156
+ // without accepting a caller-chosen hash.
157
+ const nonce = currentNonce(resolvedReservation);
158
+ if (resolvedReservation.decision !== 'committed'
159
+ || resolvedReservation.launch?.status !== 'crossed'
160
+ || !nonce) {
161
+ return 'reconciled_legacy_authority_unverifiable';
162
+ }
163
+ const contractHash = resolvedReservation.execution_contract_ref?.hash ?? evidenceDigest({
164
+ version: 'legacy-uncontracted-reservation-v1',
165
+ turn_id: resolvedReservation.turn_id,
166
+ run_id: resolvedReservation.child_ids.run_id,
167
+ epoch: resolvedReservation.epoch,
168
+ phase: resolvedReservation.phase,
169
+ iteration: resolvedReservation.iteration,
170
+ cwd: resolvedReservation.cwd,
171
+ });
172
+ const workspaceDigest = evidenceDigest({
173
+ workspace_policy: resolvedReservation.execution_contract?.workspace_policy,
174
+ cwd: resolvedReservation.cwd,
175
+ store_root: resolvedReservation.store_root,
176
+ });
177
+ const expectedLegacy = {
178
+ loop_id: resolvedReservation.loop_id,
179
+ slot_id: resolvedReservation.slot_id,
180
+ turn_id: resolvedReservation.turn_id,
181
+ assignment_id: resolvedReservation.child_ids.assignment_id,
182
+ run_id: resolvedReservation.child_ids.run_id,
183
+ nonce_digest: evidenceDigest({ launch_nonce: nonce }),
184
+ attempt_epoch: resolvedReservation.epoch,
185
+ execution_contract_hash: contractHash,
186
+ workspace_digest: workspaceDigest,
187
+ };
188
+ const actualLegacy = {
189
+ loop_id: thread.id,
190
+ slot_id: subject.slot_id,
191
+ turn_id: subject.turn_id,
192
+ assignment_id: subject.assignment_id,
193
+ run_id: subject.run_id,
194
+ nonce_digest: subject.nonce_digest,
195
+ attempt_epoch: subject.attempt_epoch,
196
+ execution_contract_hash: subject.execution_contract_hash,
197
+ workspace_digest: subject.workspace_digest,
198
+ };
199
+ const mismatch = Object.keys(expectedLegacy)
200
+ .find((field) => actualLegacy[field] !== expectedLegacy[field]);
201
+ return mismatch ? `reconciled_legacy_authority_mismatch:${mismatch}` : undefined;
202
+ }
203
+ if (chain.status !== 'active' && chain.status !== 'settled') {
204
+ return `reconciled_authority_not_usable:${chain.status}`;
205
+ }
206
+ const generation = chain.latest_generation;
207
+ let launch;
208
+ try {
209
+ launch = readLaunchDecision(resolvedReservation.store_root, generation.turn_id, generation.attempt_epoch);
210
+ }
211
+ catch {
212
+ return 'reconciled_authority_unreadable';
213
+ }
214
+ if (launch?.decision !== 'crossed')
215
+ return 'reconciled_authority_launch_not_crossed';
216
+ const expected = {
217
+ loop_id: resolvedReservation.loop_id,
218
+ slot_id: resolvedReservation.slot_id,
219
+ turn_id: generation.turn_id,
220
+ assignment_id: generation.assignment_id,
221
+ run_id: generation.run_id,
222
+ nonce_digest: evidenceDigest({ launch_nonce: generation.launch_nonce }),
223
+ attempt_epoch: generation.attempt_epoch,
224
+ execution_contract_hash: generation.contract_hash,
225
+ workspace_digest: generation.workspace_digest,
226
+ };
227
+ const actual = {
228
+ loop_id: thread.id,
229
+ slot_id: subject.slot_id,
230
+ turn_id: subject.turn_id,
231
+ assignment_id: subject.assignment_id,
232
+ run_id: subject.run_id,
233
+ nonce_digest: subject.nonce_digest,
234
+ attempt_epoch: subject.attempt_epoch,
235
+ execution_contract_hash: subject.execution_contract_hash,
236
+ workspace_digest: subject.workspace_digest,
237
+ };
238
+ const mismatch = Object.keys(expected)
239
+ .find((field) => actual[field] !== expected[field]);
240
+ return mismatch ? `reconciled_authority_mismatch:${mismatch}` : undefined;
241
+ }
242
+ function selectEligible(thread, artifacts, purpose, cwd) {
243
+ const mode = thread.evidence_policy?.mode ?? 'legacy';
244
+ const requirement = GATE_POLICIES[thread.kind].requirements[purpose];
245
+ const eligible = [];
246
+ const accepted_evidence_ids = [];
247
+ const rejected = [];
248
+ const fingerprints = new Set();
249
+ for (const artifact of artifacts) {
250
+ // critic_signal is intentionally a typed marker; unlike deliverable
251
+ // artifacts it does not require an inline/ref payload.
252
+ if (purpose !== 'critic_signal' && !hasUsableContent(artifact)) {
253
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'no_usable_content' });
254
+ continue;
255
+ }
256
+ // A present envelope is always validated, including on legacy threads.
257
+ if (artifact.evidence) {
258
+ const validity = validateArtifactEvidence(thread, artifact);
259
+ if (!validity.valid) {
260
+ rejected.push({ artifact_id: artifact.artifact_id, reason: validity.reasons.join(',') });
261
+ continue;
262
+ }
263
+ }
264
+ else if (mode === 'strict' || mode === 'shadow') {
265
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'missing_evidence' });
266
+ continue;
267
+ }
268
+ if (mode !== 'legacy') {
269
+ const envelope = artifact.evidence;
270
+ if (purpose === 'reviewer_green' && (artifact.iteration ?? 0) !== thread.iteration_count) {
271
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'stale_subject_iteration' });
272
+ continue;
273
+ }
274
+ const channelAuthorities = requirement.authorities.filter((authority) => authority.channel === envelope.producer.channel);
275
+ if (channelAuthorities.length === 0) {
276
+ rejected.push({ artifact_id: artifact.artifact_id, reason: `channel_not_allowed:${envelope.producer.channel}` });
277
+ continue;
278
+ }
279
+ const producerAuthorities = channelAuthorities.filter((authority) => authority.producer_kind === envelope.producer.kind);
280
+ if (producerAuthorities.length === 0) {
281
+ rejected.push({ artifact_id: artifact.artifact_id, reason: `producer_channel_mismatch:${envelope.producer.kind}:${envelope.producer.channel}` });
282
+ continue;
283
+ }
284
+ const authority = producerAuthorities.find((candidate) => envelope.attestations.some((item) => item.kind === candidate.attestation
285
+ && item.issuer === candidate.issuer
286
+ && item.rights.includes(requirement.right)));
287
+ if (!authority) {
288
+ rejected.push({ artifact_id: artifact.artifact_id, reason: `missing_authorized_attestation:${requirement.right}` });
289
+ continue;
290
+ }
291
+ const missingSubject = authority.required_subject_fields?.find((field) => envelope.subject[field] === undefined || envelope.subject[field] === '');
292
+ if (missingSubject) {
293
+ rejected.push({ artifact_id: artifact.artifact_id, reason: `missing_subject_binding:${missingSubject}` });
294
+ continue;
295
+ }
296
+ if (envelope.subject.slot_id) {
297
+ const slot = thread.slots.find((candidate) => candidate.slot_id === envelope.subject.slot_id);
298
+ if (!slot) {
299
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'unknown_subject_slot' });
300
+ continue;
301
+ }
302
+ if (slot.current_turn_id && envelope.subject.turn_id !== slot.current_turn_id) {
303
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'wrong_subject_turn' });
304
+ continue;
305
+ }
306
+ if (slot.assignment_id && envelope.subject.assignment_id !== slot.assignment_id) {
307
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'wrong_subject_assignment' });
308
+ continue;
309
+ }
310
+ if (slot.claim_id && envelope.subject.claim_id !== slot.claim_id) {
311
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'wrong_subject_claim' });
312
+ continue;
313
+ }
314
+ }
315
+ if (envelope.producer.channel === 'verify_command') {
316
+ let report;
317
+ try {
318
+ report = JSON.parse(artifact.body ?? '{}');
319
+ }
320
+ catch {
321
+ report = {};
322
+ }
323
+ if (report.workspace_stable !== true
324
+ || !report.command_argv
325
+ || evidenceDigest({ command: report.command_argv }) !== report.command_digest
326
+ || (thread.protocol?.verify
327
+ && evidenceDigest({ command: thread.protocol.verify.command }) !== report.command_digest)
328
+ || report.command_digest !== envelope.subject.command_digest
329
+ || report.workspace_digest !== envelope.subject.workspace_digest) {
330
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'verification_subject_mismatch' });
331
+ continue;
332
+ }
333
+ try {
334
+ if (!report.cwd || captureWorkspaceDigest(report.cwd) !== envelope.subject.workspace_digest) {
335
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'workspace_changed_after_verification' });
336
+ continue;
337
+ }
338
+ }
339
+ catch {
340
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'workspace_freshness_unavailable' });
341
+ continue;
342
+ }
343
+ }
344
+ if (envelope.producer.channel === 'reconcile_turn' && envelope.subject.execution_contract_hash === undefined) {
345
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'missing_subject_binding:execution_contract_hash' });
346
+ continue;
347
+ }
348
+ const authorityRejection = reconciledV2AuthorityRejection(thread, artifact, cwd);
349
+ if (authorityRejection) {
350
+ rejected.push({ artifact_id: artifact.artifact_id, reason: authorityRejection });
351
+ continue;
352
+ }
353
+ }
354
+ if (mode !== 'legacy') {
355
+ const fingerprint = payloadFingerprint(artifact);
356
+ if (fingerprints.has(fingerprint)) {
357
+ rejected.push({ artifact_id: artifact.artifact_id, reason: 'duplicate_payload' });
358
+ continue;
359
+ }
360
+ fingerprints.add(fingerprint);
361
+ }
362
+ eligible.push(artifact);
363
+ if (artifact.evidence)
364
+ accepted_evidence_ids.push(artifact.evidence.evidence_id);
365
+ }
366
+ return { eligible, accepted_evidence_ids, rejected };
367
+ }
368
+ function modeFor(thread) {
369
+ return thread.evidence_policy?.mode ?? 'legacy';
370
+ }
371
+ function decision(thread, condition, strictPassed, legacyPassed, set = { eligible: [], accepted_evidence_ids: [], rejected: [] }) {
372
+ const mode = modeFor(thread);
373
+ return {
374
+ passed: mode === 'legacy' || mode === 'shadow' ? legacyPassed : strictPassed,
375
+ strict_passed: strictPassed,
376
+ legacy_passed: legacyPassed,
377
+ policy_version: mode === 'legacy' ? 'legacy' : 'gate-policy-v1',
378
+ mode,
379
+ condition_digest: evidenceDigest(condition),
380
+ accepted_evidence_ids: [...new Set(set.accepted_evidence_ids)],
381
+ rejected: set.rejected,
382
+ };
383
+ }
384
+ function artifactCandidates(thread, condition) {
385
+ return thread.artifacts.filter((artifact) => {
386
+ if (artifact.type !== condition.type)
387
+ return false;
388
+ if (condition.kind === 'artifact_produced')
389
+ return artifact.phase === condition.phase;
390
+ if (condition.scope !== 'phase')
391
+ return true;
392
+ if (artifact.phase !== thread.current_phase)
393
+ return false;
394
+ const iterationAware = thread.iteration_count > 0 || thread.artifacts.some((item) => item.iteration !== undefined);
395
+ return !iterationAware || (artifact.iteration ?? 0) === thread.iteration_count;
396
+ });
397
+ }
398
+ /** One evaluator owns both terminal stop conditions and phase gates. */
399
+ export function evaluateGateCondition(thread, condition, cwd) {
400
+ if (!condition)
401
+ return decision(thread, { kind: 'absent' }, false, false);
402
+ switch (condition.kind) {
403
+ case 'phase_reached': {
404
+ const passed = thread.current_phase === condition.phase;
405
+ return decision(thread, condition, passed, passed);
406
+ }
407
+ case 'max_iterations':
408
+ case 'min_iterations': {
409
+ const passed = thread.iteration_count >= condition.n;
410
+ return decision(thread, condition, passed, passed);
411
+ }
412
+ case 'no_open_questions': {
413
+ const passed = thread.open_questions.length === 0;
414
+ return decision(thread, condition, passed, passed);
415
+ }
416
+ case 'manual':
417
+ return decision(thread, condition, false, false);
418
+ case 'reviewer_green': {
419
+ const candidates = thread.artifacts.filter((artifact) => artifact.type === 'verdict' && /^accepted(?:\b|[:\s])/.test((artifact.body ?? '').trim().toLowerCase()));
420
+ const set = selectEligible(thread, candidates, 'reviewer_green', cwd);
421
+ return decision(thread, condition, set.eligible.length > 0, legacyEligibleCount(thread, candidates, 'reviewer_green', cwd) > 0, set);
422
+ }
423
+ case 'artifact_produced': {
424
+ const candidates = artifactCandidates(thread, condition);
425
+ const set = selectEligible(thread, candidates, 'artifact', cwd);
426
+ return decision(thread, condition, set.eligible.length > 0, legacyEligibleCount(thread, candidates, 'artifact', cwd) > 0, set);
427
+ }
428
+ case 'min_artifacts_by_type': {
429
+ const candidates = artifactCandidates(thread, condition);
430
+ const set = selectEligible(thread, candidates, 'artifact', cwd);
431
+ const requiredLanes = thread.kind === 'implementation' && condition.type === 'verify_report'
432
+ ? [...new Set(thread.slots.map((slot) => slot.lane).filter((lane) => Boolean(lane)))]
433
+ : [];
434
+ const covers = (artifacts) => {
435
+ if (requiredLanes.length === 0)
436
+ return artifacts.length >= condition.n;
437
+ const reported = new Set(artifacts.flatMap((artifact) => {
438
+ try {
439
+ const lane = JSON.parse(artifact.body ?? '{}').lane;
440
+ return lane ? [lane] : [];
441
+ }
442
+ catch {
443
+ return [];
444
+ }
445
+ }));
446
+ return requiredLanes.every((lane) => reported.has(lane));
447
+ };
448
+ return decision(thread, condition, covers(set.eligible), covers(legacyEligibleArtifacts(thread, candidates, 'artifact', cwd)), set);
449
+ }
450
+ case 'any': {
451
+ const children = condition.conditions.map((child) => evaluateGateCondition(thread, child, cwd));
452
+ return decision(thread, condition, children.some((child) => child.strict_passed ?? child.passed), children.some((child) => child.legacy_passed ?? child.passed), {
453
+ eligible: [],
454
+ accepted_evidence_ids: children.flatMap((child) => child.accepted_evidence_ids),
455
+ rejected: children.flatMap((child) => child.rejected),
456
+ });
457
+ }
458
+ case 'all': {
459
+ const children = condition.conditions.map((child) => evaluateGateCondition(thread, child, cwd));
460
+ return decision(thread, condition, children.every((child) => child.strict_passed ?? child.passed), children.every((child) => child.legacy_passed ?? child.passed), {
461
+ eligible: [],
462
+ accepted_evidence_ids: children.flatMap((child) => child.accepted_evidence_ids),
463
+ rejected: children.flatMap((child) => child.rejected),
464
+ });
465
+ }
466
+ }
467
+ }
468
+ export function eligibleArtifactsForPurpose(thread, artifacts, purpose, cwd) {
469
+ return selectEligible(thread, artifacts, purpose, cwd);
470
+ }
471
+ export function evaluateCommandGreen(thread, iteration, cwd) {
472
+ const candidates = thread.artifacts.filter((artifact) => {
473
+ if (artifact.type !== 'verify_report' || (artifact.iteration ?? 0) !== iteration)
474
+ return false;
475
+ try {
476
+ return JSON.parse(artifact.body ?? '{}').passed === true;
477
+ }
478
+ catch {
479
+ return false;
480
+ }
481
+ });
482
+ const set = selectEligible(thread, candidates, 'command_green', cwd);
483
+ const requiredLanes = thread.kind === 'implementation'
484
+ ? [...new Set(thread.slots.map((slot) => slot.lane).filter((lane) => Boolean(lane)))]
485
+ : [];
486
+ const greenLanes = new Set(set.eligible.flatMap((artifact) => {
487
+ try {
488
+ const lane = JSON.parse(artifact.body ?? '{}').lane;
489
+ return lane ? [lane] : [];
490
+ }
491
+ catch {
492
+ return [];
493
+ }
494
+ }));
495
+ const allLanesGreen = requiredLanes.length === 0
496
+ ? set.eligible.length > 0
497
+ : requiredLanes.every((lane) => greenLanes.has(lane));
498
+ const legacyCandidates = legacyEligibleArtifacts(thread, candidates, 'command_green', cwd);
499
+ const legacyGreenLanes = new Set(legacyCandidates.flatMap((artifact) => {
500
+ try {
501
+ const lane = JSON.parse(artifact.body ?? '{}').lane;
502
+ return lane ? [lane] : [];
503
+ }
504
+ catch {
505
+ return [];
506
+ }
507
+ }));
508
+ const legacyAllLanesGreen = requiredLanes.length === 0
509
+ ? legacyCandidates.length > 0
510
+ : requiredLanes.every((lane) => legacyGreenLanes.has(lane));
511
+ return decision(thread, { kind: 'command_green', iteration }, allLanesGreen, legacyAllLanesGreen, set);
512
+ }
513
+ export function evaluateCriticSignal(thread, iteration, cwd) {
514
+ const candidates = thread.artifacts.filter((artifact) => artifact.type === 'critic_signal' && (artifact.iteration ?? 0) === iteration);
515
+ const set = selectEligible(thread, candidates, 'critic_signal', cwd);
516
+ return decision(thread, { kind: 'critic_signal', iteration }, set.eligible.length > 0, legacyEligibleCount(thread, candidates, 'critic_signal', cwd) > 0, set);
517
+ }
518
+ export function evaluateNoNewCritique(thread, iteration, cwd) {
519
+ const candidates = thread.artifacts.filter((artifact) => artifact.type === 'critique' && (artifact.iteration ?? 0) === iteration);
520
+ const set = selectEligible(thread, candidates, 'artifact', cwd);
521
+ const closureCandidates = thread.artifacts.filter((artifact) => artifact.type === 'critique_window_closed' && (artifact.iteration ?? 0) === iteration);
522
+ const closure = selectEligible(thread, closureCandidates, 'artifact', cwd);
523
+ // Invalid/replayed critiques cannot be used to manufacture a negative convergence signal.
524
+ const strictPassed = set.eligible.length === 0
525
+ && set.rejected.length === 0
526
+ && closure.eligible.length > 0;
527
+ return decision(thread, { kind: 'no_new_critique_artifacts', iteration }, strictPassed, candidates.length === 0, {
528
+ eligible: closure.eligible,
529
+ accepted_evidence_ids: [...set.accepted_evidence_ids, ...closure.accepted_evidence_ids],
530
+ rejected: [...set.rejected, ...closure.rejected],
531
+ });
532
+ }
533
+ //# sourceMappingURL=gate-policy.js.map