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,509 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { z } from 'zod';
5
+ import { memoryDir, writeFileAtomic } from '../io.js';
6
+ import { nowISO } from '../ids.js';
7
+ /**
8
+ * Additive AttemptAuthority v2 primitive.
9
+ *
10
+ * Immutable launch/close cells are authoritative. `head.json` is deliberately
11
+ * only a rebuildable read cache. Callers may prepare generations and cells in
12
+ * parallel, but publication is one no-clobber hard-link CAS per decision.
13
+ */
14
+ export const ATTEMPT_AUTHORITY_VERSION = 2;
15
+ const SAFE_PATH_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
16
+ export const AuthorityHomeSchema = z.object({
17
+ store_instance_id: z.string().min(1),
18
+ device_id: z.string().min(1),
19
+ }).strict();
20
+ export const GenerationFenceSchema = z.object({
21
+ turn_id: z.string().regex(SAFE_PATH_SEGMENT),
22
+ assignment_id: z.string().min(1),
23
+ attempt_epoch: z.number().int().nonnegative(),
24
+ run_id: z.string().min(1),
25
+ launch_nonce: z.string().min(1),
26
+ contract_hash: z.string().min(1),
27
+ workspace_id: z.string().min(1),
28
+ workspace_path: z.string().min(1).refine((value) => path.isAbsolute(value), {
29
+ message: 'workspace_path must be absolute',
30
+ }),
31
+ workspace_digest: z.string().min(1),
32
+ authority_home: AuthorityHomeSchema,
33
+ }).strict();
34
+ export const AttemptGenerationSchema = GenerationFenceSchema.extend({
35
+ schema_version: z.literal(ATTEMPT_AUTHORITY_VERSION),
36
+ generation_kind: z.literal('attempt_generation'),
37
+ created_at: z.string().min(1),
38
+ }).strict();
39
+ export const LaunchDecisionCellSchema = z.object({
40
+ schema_version: z.literal(ATTEMPT_AUTHORITY_VERSION),
41
+ cell_kind: z.literal('launch_decision'),
42
+ fence: GenerationFenceSchema,
43
+ generation_digest: z.string().regex(/^[a-f0-9]{64}$/),
44
+ decision: z.enum(['crossed', 'revoked']),
45
+ actor: z.string().min(1),
46
+ cause: z.string().min(1),
47
+ decided_at: z.string().min(1),
48
+ }).strict();
49
+ const CloseCellBaseSchema = z.object({
50
+ schema_version: z.literal(ATTEMPT_AUTHORITY_VERSION),
51
+ cell_kind: z.literal('close_decision'),
52
+ fence: GenerationFenceSchema,
53
+ generation_digest: z.string().regex(/^[a-f0-9]{64}$/),
54
+ actor: z.string().min(1),
55
+ cause: z.string().min(1),
56
+ result_digest: z.string().min(1).optional(),
57
+ decided_at: z.string().min(1),
58
+ });
59
+ const SettledCloseCellSchema = CloseCellBaseSchema.extend({
60
+ decision: z.literal('settled'),
61
+ next_generation: z.never().optional(),
62
+ }).strict();
63
+ const CancelledCloseCellSchema = CloseCellBaseSchema.extend({
64
+ decision: z.literal('cancelled'),
65
+ next_generation: z.never().optional(),
66
+ }).strict();
67
+ const TakeoverCloseCellSchema = CloseCellBaseSchema.extend({
68
+ decision: z.literal('takeover'),
69
+ next_generation: AttemptGenerationSchema,
70
+ }).strict();
71
+ const RetryCloseCellSchema = CloseCellBaseSchema.extend({
72
+ decision: z.literal('retry'),
73
+ next_generation: AttemptGenerationSchema,
74
+ }).strict();
75
+ export const CloseDecisionCellSchema = z.discriminatedUnion('decision', [
76
+ SettledCloseCellSchema,
77
+ CancelledCloseCellSchema,
78
+ TakeoverCloseCellSchema,
79
+ RetryCloseCellSchema,
80
+ ]);
81
+ /** Immutable terminal evidence published before close(epoch)=settled. */
82
+ export const AttemptResultEvidenceCellSchema = z.object({
83
+ schema_version: z.literal(ATTEMPT_AUTHORITY_VERSION),
84
+ cell_kind: z.literal('attempt_result_evidence'),
85
+ fence: GenerationFenceSchema,
86
+ result: z.record(z.string(), z.unknown()),
87
+ }).strict();
88
+ export const AttemptGenerationHeadSchema = z.object({
89
+ schema_version: z.literal(ATTEMPT_AUTHORITY_VERSION),
90
+ projection_kind: z.literal('attempt_generation_head'),
91
+ authoritative: z.literal(false),
92
+ turn_id: z.string().regex(SAFE_PATH_SEGMENT),
93
+ status: z.enum(['active', 'settled', 'cancelled']),
94
+ latest_epoch: z.number().int().nonnegative(),
95
+ latest_run_id: z.string().min(1),
96
+ active_run_id: z.string().min(1).nullable(),
97
+ latest_generation_digest: z.string().regex(/^[a-f0-9]{64}$/),
98
+ rebuilt_at: z.string().min(1),
99
+ }).strict();
100
+ export class AttemptGenerationError extends Error {
101
+ code;
102
+ constructor(code, message) {
103
+ super(message);
104
+ this.code = code;
105
+ this.name = 'AttemptGenerationError';
106
+ }
107
+ }
108
+ export class ImmutableCellPublishError extends Error {
109
+ code;
110
+ cell_path;
111
+ constructor(code, cell_path, message, options) {
112
+ super(message, options);
113
+ this.code = code;
114
+ this.cell_path = cell_path;
115
+ this.name = 'ImmutableCellPublishError';
116
+ }
117
+ }
118
+ export class CorruptAttemptCellError extends Error {
119
+ cell_path;
120
+ constructor(cell_path, message, options) {
121
+ super(message, options);
122
+ this.cell_path = cell_path;
123
+ this.name = 'CorruptAttemptCellError';
124
+ }
125
+ }
126
+ function canonicalize(value) {
127
+ if (Array.isArray(value))
128
+ return value.map(canonicalize);
129
+ if (!value || typeof value !== 'object')
130
+ return value;
131
+ return Object.fromEntries(Object.entries(value)
132
+ .sort(([left], [right]) => left.localeCompare(right))
133
+ .map(([key, child]) => [key, canonicalize(child)]));
134
+ }
135
+ export function generationDigest(generation) {
136
+ const parsed = AttemptGenerationSchema.parse(generation);
137
+ return crypto.createHash('sha256').update(JSON.stringify(canonicalize(parsed))).digest('hex');
138
+ }
139
+ export function attemptResultEvidenceDigest(cell) {
140
+ const parsed = AttemptResultEvidenceCellSchema.parse(cell);
141
+ return crypto.createHash('sha256').update(JSON.stringify(canonicalize(parsed))).digest('hex');
142
+ }
143
+ function stableId(prefix, material) {
144
+ const digest = crypto.createHash('sha256').update(material).digest('hex').slice(0, 16);
145
+ return `${prefix}_${digest}`;
146
+ }
147
+ /** Compatible with the existing logical Assignment derivation. */
148
+ export function deriveGenerationAssignmentId(turnId) {
149
+ return stableId('asgn', `${turnId}:assignment`);
150
+ }
151
+ /** Epoch zero preserves the existing run id; every later epoch is fresh. */
152
+ export function deriveGenerationRunId(turnId, attemptEpoch) {
153
+ const salt = attemptEpoch === 0 ? 'run' : `run:${attemptEpoch}`;
154
+ return stableId('run', `${turnId}:${salt}`);
155
+ }
156
+ export function deriveGenerationWorkspaceId(turnId, attemptEpoch) {
157
+ return stableId('wsp', `${turnId}:workspace:${attemptEpoch}`);
158
+ }
159
+ export function prepareInitialGeneration(input) {
160
+ return AttemptGenerationSchema.parse({
161
+ schema_version: ATTEMPT_AUTHORITY_VERSION,
162
+ generation_kind: 'attempt_generation',
163
+ turn_id: input.turn_id,
164
+ assignment_id: deriveGenerationAssignmentId(input.turn_id),
165
+ attempt_epoch: 0,
166
+ run_id: deriveGenerationRunId(input.turn_id, 0),
167
+ launch_nonce: input.launch_nonce ?? crypto.randomUUID(),
168
+ contract_hash: input.contract_hash,
169
+ workspace_id: deriveGenerationWorkspaceId(input.turn_id, 0),
170
+ workspace_path: input.workspace_path,
171
+ workspace_digest: input.workspace_digest,
172
+ authority_home: AuthorityHomeSchema.parse(input.authority_home),
173
+ created_at: input.created_at ?? nowISO(),
174
+ });
175
+ }
176
+ export function prepareNextGeneration(current, input) {
177
+ const parsed = AttemptGenerationSchema.parse(current);
178
+ const attemptEpoch = parsed.attempt_epoch + 1;
179
+ return AttemptGenerationSchema.parse({
180
+ ...parsed,
181
+ attempt_epoch: attemptEpoch,
182
+ run_id: deriveGenerationRunId(parsed.turn_id, attemptEpoch),
183
+ launch_nonce: input.launch_nonce ?? crypto.randomUUID(),
184
+ contract_hash: input.contract_hash,
185
+ workspace_id: deriveGenerationWorkspaceId(parsed.turn_id, attemptEpoch),
186
+ workspace_path: input.workspace_path,
187
+ workspace_digest: input.workspace_digest,
188
+ created_at: input.created_at ?? nowISO(),
189
+ });
190
+ }
191
+ export function fenceForGeneration(generation) {
192
+ const parsed = AttemptGenerationSchema.parse(generation);
193
+ return GenerationFenceSchema.parse({
194
+ turn_id: parsed.turn_id,
195
+ assignment_id: parsed.assignment_id,
196
+ attempt_epoch: parsed.attempt_epoch,
197
+ run_id: parsed.run_id,
198
+ launch_nonce: parsed.launch_nonce,
199
+ contract_hash: parsed.contract_hash,
200
+ workspace_id: parsed.workspace_id,
201
+ workspace_path: parsed.workspace_path,
202
+ workspace_digest: parsed.workspace_digest,
203
+ authority_home: parsed.authority_home,
204
+ });
205
+ }
206
+ function sameJson(left, right) {
207
+ return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
208
+ }
209
+ function canonicalWorkspacePath(workspacePath) {
210
+ const absolute = path.resolve(workspacePath);
211
+ let canonical;
212
+ try {
213
+ canonical = fs.realpathSync.native(absolute);
214
+ }
215
+ catch {
216
+ canonical = absolute;
217
+ }
218
+ return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
219
+ }
220
+ export function fenceMatchesGeneration(generation, fence) {
221
+ return sameJson(fenceForGeneration(generation), GenerationFenceSchema.parse(fence));
222
+ }
223
+ export function assertFenceMatchesGeneration(generation, fence) {
224
+ if (!fenceMatchesGeneration(generation, fence)) {
225
+ throw new AttemptGenerationError('fenced', 'generation fence tuple does not match the active generation');
226
+ }
227
+ }
228
+ function assertValidSuccessor(current, next) {
229
+ if (!sameJson(current.authority_home, next.authority_home)) {
230
+ throw new AttemptGenerationError('authority_home_mismatch', 'next generation changes authority_home');
231
+ }
232
+ const invalid = [
233
+ current.turn_id !== next.turn_id && 'turn_id',
234
+ current.assignment_id !== next.assignment_id && 'assignment_id',
235
+ next.attempt_epoch !== current.attempt_epoch + 1 && 'attempt_epoch',
236
+ next.run_id === current.run_id && 'run_id',
237
+ next.launch_nonce === current.launch_nonce && 'launch_nonce',
238
+ next.workspace_id === current.workspace_id && 'workspace_id',
239
+ canonicalWorkspacePath(next.workspace_path) === canonicalWorkspacePath(current.workspace_path)
240
+ && 'workspace_path',
241
+ ].find(Boolean);
242
+ if (invalid) {
243
+ throw new AttemptGenerationError('invalid_transition', `invalid generation successor: ${invalid}`);
244
+ }
245
+ }
246
+ export function prepareLaunchDecision(generation, input) {
247
+ return LaunchDecisionCellSchema.parse({
248
+ schema_version: ATTEMPT_AUTHORITY_VERSION,
249
+ cell_kind: 'launch_decision',
250
+ fence: fenceForGeneration(generation),
251
+ generation_digest: generationDigest(generation),
252
+ decision: input.decision,
253
+ actor: input.actor,
254
+ cause: input.cause,
255
+ decided_at: input.decided_at ?? nowISO(),
256
+ });
257
+ }
258
+ export function prepareCloseDecision(generation, input) {
259
+ if (input.decision === 'takeover' || input.decision === 'retry') {
260
+ assertValidSuccessor(generation, input.next_generation);
261
+ }
262
+ return CloseDecisionCellSchema.parse({
263
+ schema_version: ATTEMPT_AUTHORITY_VERSION,
264
+ cell_kind: 'close_decision',
265
+ fence: fenceForGeneration(generation),
266
+ generation_digest: generationDigest(generation),
267
+ decision: input.decision,
268
+ actor: input.actor,
269
+ cause: input.cause,
270
+ result_digest: input.result_digest,
271
+ ...((input.decision === 'takeover' || input.decision === 'retry')
272
+ ? { next_generation: input.next_generation }
273
+ : {}),
274
+ decided_at: input.decided_at ?? nowISO(),
275
+ });
276
+ }
277
+ function attemptDir(cwd, turnId) {
278
+ if (!SAFE_PATH_SEGMENT.test(turnId))
279
+ throw new Error(`unsafe turn_id path segment: ${turnId}`);
280
+ return path.join(memoryDir(cwd), 'loops', 'attempt-generations', turnId);
281
+ }
282
+ export function launchDecisionCellPath(cwd, turnId, attemptEpoch) {
283
+ return path.join(attemptDir(cwd, turnId), `launch-${attemptEpoch}.decision.json`);
284
+ }
285
+ export function initialGenerationCellPath(cwd, turnId) {
286
+ return path.join(attemptDir(cwd, turnId), 'generation-0.json');
287
+ }
288
+ export function closeDecisionCellPath(cwd, turnId, attemptEpoch) {
289
+ return path.join(attemptDir(cwd, turnId), `close-${attemptEpoch}.decision.json`);
290
+ }
291
+ export function attemptResultEvidenceCellPath(cwd, turnId, attemptEpoch, digest) {
292
+ if (!/^[a-f0-9]{64}$/.test(digest))
293
+ throw new Error(`unsafe result evidence digest: ${digest}`);
294
+ return path.join(attemptDir(cwd, turnId), `result-${attemptEpoch}-${digest}.json`);
295
+ }
296
+ export function attemptGenerationHeadPath(cwd, turnId) {
297
+ return path.join(attemptDir(cwd, turnId), 'head.json');
298
+ }
299
+ const HARDLINK_UNSUPPORTED = new Set(['ENOTSUP', 'EOPNOTSUPP', 'ENOSYS', 'EXDEV', 'EINVAL', 'EPERM', 'EACCES']);
300
+ function errorCode(error) {
301
+ return error instanceof Error && 'code' in error
302
+ ? String(error.code)
303
+ : undefined;
304
+ }
305
+ function readImmutableCell(filePath, schema) {
306
+ if (!fs.existsSync(filePath))
307
+ return undefined;
308
+ try {
309
+ return schema.parse(JSON.parse(fs.readFileSync(filePath, 'utf8')));
310
+ }
311
+ catch (error) {
312
+ throw new CorruptAttemptCellError(filePath, `immutable attempt cell is corrupt: ${filePath}`, { cause: error });
313
+ }
314
+ }
315
+ function publishImmutableCell(finalPath, cell, schema, options = {}) {
316
+ const parsed = schema.parse(cell);
317
+ const dir = path.dirname(finalPath);
318
+ fs.mkdirSync(dir, { recursive: true });
319
+ const tempPath = path.join(dir, `.${path.basename(finalPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
320
+ const bytes = Buffer.from(`${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
321
+ const fd = fs.openSync(tempPath, 'wx');
322
+ try {
323
+ let offset = 0;
324
+ while (offset < bytes.length)
325
+ offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
326
+ fs.fsyncSync(fd);
327
+ }
328
+ finally {
329
+ fs.closeSync(fd);
330
+ }
331
+ if (options.simulateCrashAfterTempFsync) {
332
+ throw new ImmutableCellPublishError('simulated_crash', finalPath, `simulated crash after temp fsync for ${finalPath}`);
333
+ }
334
+ try {
335
+ (options.linkSync ?? fs.linkSync)(tempPath, finalPath);
336
+ try {
337
+ fs.unlinkSync(tempPath);
338
+ }
339
+ catch { /* orphan cleanup is best-effort */ }
340
+ return { won: true, cell: parsed };
341
+ }
342
+ catch (error) {
343
+ try {
344
+ fs.unlinkSync(tempPath);
345
+ }
346
+ catch { /* best-effort */ }
347
+ const code = errorCode(error);
348
+ if (code === 'EEXIST') {
349
+ const incumbent = readImmutableCell(finalPath, schema);
350
+ if (!incumbent) {
351
+ throw new CorruptAttemptCellError(finalPath, `exclusive-create lost but incumbent is absent: ${finalPath}`);
352
+ }
353
+ return { won: false, cell: incumbent };
354
+ }
355
+ if (code && HARDLINK_UNSUPPORTED.has(code)) {
356
+ throw new ImmutableCellPublishError('hardlink_unsupported', finalPath, `filesystem cannot atomically publish immutable attempt cells via hard-link (${code})`, { cause: error });
357
+ }
358
+ throw new ImmutableCellPublishError('publish_failed', finalPath, `failed to publish immutable attempt cell: ${finalPath}`, { cause: error });
359
+ }
360
+ }
361
+ function validateLaunchCellForGeneration(cell, generation) {
362
+ assertFenceMatchesGeneration(generation, cell.fence);
363
+ if (cell.generation_digest !== generationDigest(generation)) {
364
+ throw new CorruptAttemptCellError('launch decision', 'launch decision generation digest mismatch');
365
+ }
366
+ }
367
+ function validateCloseCellForGeneration(cell, generation) {
368
+ assertFenceMatchesGeneration(generation, cell.fence);
369
+ if (cell.generation_digest !== generationDigest(generation)) {
370
+ throw new CorruptAttemptCellError('close decision', 'close decision generation digest mismatch');
371
+ }
372
+ if (cell.decision === 'takeover' || cell.decision === 'retry') {
373
+ assertValidSuccessor(generation, cell.next_generation);
374
+ }
375
+ }
376
+ export function publishPreparedLaunchDecision(cwd, generation, cell, options) {
377
+ const parsedGeneration = AttemptGenerationSchema.parse(generation);
378
+ const parsedCell = LaunchDecisionCellSchema.parse(cell);
379
+ validateLaunchCellForGeneration(parsedCell, parsedGeneration);
380
+ const result = publishImmutableCell(launchDecisionCellPath(cwd, parsedGeneration.turn_id, parsedGeneration.attempt_epoch), parsedCell, LaunchDecisionCellSchema, options);
381
+ validateLaunchCellForGeneration(result.cell, parsedGeneration);
382
+ return result;
383
+ }
384
+ /**
385
+ * Publish the immutable generation-zero anchor. A copied/mutable reservation
386
+ * record is never used as the v2 chain root; every takeover resolves from this
387
+ * no-clobber cell and then follows close(epoch) successors.
388
+ */
389
+ export function publishInitialGeneration(cwd, generation, options) {
390
+ const parsed = AttemptGenerationSchema.parse(generation);
391
+ if (parsed.attempt_epoch !== 0) {
392
+ throw new AttemptGenerationError('invalid_transition', 'initial generation must have attempt_epoch 0');
393
+ }
394
+ const result = publishImmutableCell(initialGenerationCellPath(cwd, parsed.turn_id), parsed, AttemptGenerationSchema, options);
395
+ if (!sameJson(result.cell, parsed)) {
396
+ throw new AttemptGenerationError('invalid_transition', `a different generation-zero anchor already exists for ${parsed.turn_id}`);
397
+ }
398
+ return result;
399
+ }
400
+ export function readInitialGeneration(cwd, turnId) {
401
+ return readImmutableCell(initialGenerationCellPath(cwd, turnId), AttemptGenerationSchema);
402
+ }
403
+ export function publishAttemptResultEvidence(cwd, generation, result, options) {
404
+ const cell = AttemptResultEvidenceCellSchema.parse({
405
+ schema_version: ATTEMPT_AUTHORITY_VERSION,
406
+ cell_kind: 'attempt_result_evidence',
407
+ fence: fenceForGeneration(generation),
408
+ result,
409
+ });
410
+ const digest = attemptResultEvidenceDigest(cell);
411
+ const published = publishImmutableCell(attemptResultEvidenceCellPath(cwd, generation.turn_id, generation.attempt_epoch, digest), cell, AttemptResultEvidenceCellSchema, options);
412
+ if (attemptResultEvidenceDigest(published.cell) !== digest) {
413
+ throw new CorruptAttemptCellError('result evidence', 'result evidence digest mismatch');
414
+ }
415
+ return { ...published, digest };
416
+ }
417
+ export function readAttemptResultEvidence(cwd, turnId, attemptEpoch, digest) {
418
+ const cell = readImmutableCell(attemptResultEvidenceCellPath(cwd, turnId, attemptEpoch, digest), AttemptResultEvidenceCellSchema);
419
+ if (cell && attemptResultEvidenceDigest(cell) !== digest) {
420
+ throw new CorruptAttemptCellError('result evidence', 'result evidence path digest mismatch');
421
+ }
422
+ return cell;
423
+ }
424
+ export function publishPreparedCloseDecision(cwd, generation, cell, options) {
425
+ const parsedGeneration = AttemptGenerationSchema.parse(generation);
426
+ const parsedCell = CloseDecisionCellSchema.parse(cell);
427
+ validateCloseCellForGeneration(parsedCell, parsedGeneration);
428
+ const result = publishImmutableCell(closeDecisionCellPath(cwd, parsedGeneration.turn_id, parsedGeneration.attempt_epoch), parsedCell, CloseDecisionCellSchema, options);
429
+ validateCloseCellForGeneration(result.cell, parsedGeneration);
430
+ return result;
431
+ }
432
+ export function readLaunchDecision(cwd, turnId, attemptEpoch) {
433
+ return readImmutableCell(launchDecisionCellPath(cwd, turnId, attemptEpoch), LaunchDecisionCellSchema);
434
+ }
435
+ export function readCloseDecision(cwd, turnId, attemptEpoch) {
436
+ return readImmutableCell(closeDecisionCellPath(cwd, turnId, attemptEpoch), CloseDecisionCellSchema);
437
+ }
438
+ export function listAttemptGenerations(cwd, initialGeneration) {
439
+ const generations = [];
440
+ let current = AttemptGenerationSchema.parse(initialGeneration);
441
+ for (let depth = 0; depth < 10_000; depth++) {
442
+ generations.push(current);
443
+ const close = readCloseDecision(cwd, current.turn_id, current.attempt_epoch);
444
+ if (!close || (close.decision !== 'takeover' && close.decision !== 'retry'))
445
+ return generations;
446
+ validateCloseCellForGeneration(close, current);
447
+ current = close.next_generation;
448
+ }
449
+ throw new AttemptGenerationError('generation_chain_too_deep', `attempt generation chain exceeds safety bound for ${initialGeneration.turn_id}`);
450
+ }
451
+ /** Reconstruct authority exclusively from immutable close cells; head is ignored. */
452
+ export function resolveGenerationChain(cwd, initialGeneration) {
453
+ let current = AttemptGenerationSchema.parse(initialGeneration);
454
+ for (let depth = 0; depth < 10_000; depth++) {
455
+ const close = readCloseDecision(cwd, current.turn_id, current.attempt_epoch);
456
+ if (!close)
457
+ return { latest_generation: current, status: 'active' };
458
+ validateCloseCellForGeneration(close, current);
459
+ if (close.decision === 'takeover' || close.decision === 'retry') {
460
+ current = close.next_generation;
461
+ continue;
462
+ }
463
+ return {
464
+ latest_generation: current,
465
+ status: close.decision,
466
+ terminal_cell: close,
467
+ };
468
+ }
469
+ throw new AttemptGenerationError('generation_chain_too_deep', `attempt generation chain exceeds safety bound for ${initialGeneration.turn_id}`);
470
+ }
471
+ export function resolveTurnGenerationChain(cwd, turnId) {
472
+ const initial = readInitialGeneration(cwd, turnId);
473
+ return initial ? resolveGenerationChain(cwd, initial) : undefined;
474
+ }
475
+ /**
476
+ * Replace the non-authoritative head cache from immutable cells. Failure or
477
+ * corruption of head.json never changes the reconstructed authority.
478
+ */
479
+ export function rebuildAttemptGenerationHead(cwd, initialGeneration) {
480
+ const resolved = resolveGenerationChain(cwd, initialGeneration);
481
+ const latest = resolved.latest_generation;
482
+ const head = AttemptGenerationHeadSchema.parse({
483
+ schema_version: ATTEMPT_AUTHORITY_VERSION,
484
+ projection_kind: 'attempt_generation_head',
485
+ authoritative: false,
486
+ turn_id: latest.turn_id,
487
+ status: resolved.status,
488
+ latest_epoch: latest.attempt_epoch,
489
+ latest_run_id: latest.run_id,
490
+ active_run_id: resolved.status === 'active' ? latest.run_id : null,
491
+ latest_generation_digest: generationDigest(latest),
492
+ rebuilt_at: nowISO(),
493
+ });
494
+ writeFileAtomic(attemptGenerationHeadPath(cwd, latest.turn_id), `${JSON.stringify(head, null, 2)}\n`);
495
+ return head;
496
+ }
497
+ /** Invalid or absent head caches are ignored; callers rebuild from cells. */
498
+ export function readAttemptGenerationHead(cwd, turnId) {
499
+ const filePath = attemptGenerationHeadPath(cwd, turnId);
500
+ if (!fs.existsSync(filePath))
501
+ return undefined;
502
+ try {
503
+ return AttemptGenerationHeadSchema.parse(JSON.parse(fs.readFileSync(filePath, 'utf8')));
504
+ }
505
+ catch {
506
+ return undefined;
507
+ }
508
+ }
509
+ //# sourceMappingURL=attempt-generations.js.map