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
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Functional authority boundary for one physical loop-turn attempt.
3
+ *
4
+ * This facade deliberately stores nothing of its own. TurnReservation remains
5
+ * the canonical record and the Loop Engine remains responsible for phases,
6
+ * gates, retries and convergence. The only decision made here is whether one
7
+ * caller won the irreversible launch crossing.
8
+ */
9
+ import crypto from 'node:crypto';
10
+ import { spawnSync } from 'node:child_process';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { ExecutionContractSchema, executionContractRef, } from '../execution-contract.js';
14
+ import { abortReservation, armLaunch, attemptStatus, commitReservation, consumeLaunchGrantWithProjection, currentNonce, evidenceMatchesAttempt, getReservation, launchGrant, reserve, revokeLaunchGrant, LaunchFenceError, ReservationStateError, } from './attempt-reservation.js';
15
+ import { AttemptGenerationError, fenceForGeneration, generationDigest, prepareCloseDecision, prepareInitialGeneration, prepareLaunchDecision, prepareNextGeneration, publishInitialGeneration, publishAttemptResultEvidence, publishPreparedCloseDecision, publishPreparedLaunchDecision, readCloseDecision, readInitialGeneration, readAttemptResultEvidence, readLaunchDecision, rebuildAttemptGenerationHead, listAttemptGenerations, resolveTurnGenerationChain, } from './attempt-generations.js';
16
+ import { assertAttemptAuthorityV2Writable, } from './attempt-rollout.js';
17
+ function sameExpectedArtifacts(a, b) {
18
+ const canonical = (items) => JSON.stringify(items.map((item) => ({
19
+ logical_name: item.logical_name,
20
+ worker_path: item.worker_path,
21
+ loop_artifact_type: item.loop_artifact_type,
22
+ schema_id: item.schema_id,
23
+ completion_policy: item.completion_policy,
24
+ sha256: item.sha256,
25
+ })));
26
+ return canonical(a) === canonical(b ?? []);
27
+ }
28
+ function assertCompatible(existing, input) {
29
+ const mismatch = [
30
+ existing.loop_id !== input.loop_id && 'loop_id',
31
+ existing.slot_id !== input.slot_id && 'slot_id',
32
+ existing.target_slot_generation !== input.target_slot_generation && 'target_slot_generation',
33
+ existing.agent !== input.agent && 'agent',
34
+ existing.agent_id !== input.agent_id && 'agent_id',
35
+ existing.claim_id !== input.claim_id && 'claim_id',
36
+ existing.phase !== input.phase && 'phase',
37
+ existing.iteration !== input.iteration && 'iteration',
38
+ existing.completion_mode !== (input.completion_mode ?? 'file') && 'completion_mode',
39
+ !sameExpectedArtifacts(existing.expected_artifacts, input.expected_artifacts) && 'expected_artifacts',
40
+ existing.execution_contract_ref !== undefined
41
+ && input.execution_contract_ref !== undefined
42
+ && JSON.stringify(existing.execution_contract_ref) !== JSON.stringify(input.execution_contract_ref)
43
+ && 'execution_contract_ref',
44
+ existing.capability_snapshot !== undefined
45
+ && input.capability_snapshot !== undefined
46
+ && JSON.stringify(existing.capability_snapshot) !== JSON.stringify(input.capability_snapshot)
47
+ && 'capability_snapshot',
48
+ ].find(Boolean);
49
+ if (mismatch) {
50
+ throw new ReservationStateError(input.turn_id, 'reservation_exists', `prepareAttempt: existing reservation ${input.turn_id} has incompatible ${mismatch}`);
51
+ }
52
+ }
53
+ /**
54
+ * Reserve-or-adopt, commit and arm one attempt generation.
55
+ *
56
+ * Adoption is allowed only for an identical immutable contract. A crossed
57
+ * generation is returned as adopted so callers can repair/read projections,
58
+ * but projectAndCross will never award launch authority twice.
59
+ */
60
+ export function prepareAttempt(input, cwd) {
61
+ const actor = input.authority_actor ?? input.agent_id ?? input.agent;
62
+ let adopted = false;
63
+ try {
64
+ reserve(input, cwd);
65
+ }
66
+ catch (error) {
67
+ const existing = getReservation(input.turn_id, cwd);
68
+ if (!(error instanceof ReservationStateError) || error.code !== 'reservation_exists' || !existing) {
69
+ throw error;
70
+ }
71
+ assertCompatible(existing, input);
72
+ adopted = true;
73
+ }
74
+ input.on_stage?.('reserved');
75
+ const committed = commitReservation(input.turn_id, cwd, actor);
76
+ input.on_stage?.('committed');
77
+ let grant = launchGrant(input.turn_id, cwd);
78
+ if (!grant || grant.status === 'revoked') {
79
+ const nextEpoch = (grant?.epoch ?? -1) + 1;
80
+ try {
81
+ armLaunch(input.turn_id, {
82
+ epoch: nextEpoch,
83
+ lease_deadline: input.grant_lease_deadline,
84
+ }, cwd, actor);
85
+ }
86
+ catch (error) {
87
+ // A concurrent preparer may have armed the same committed reservation.
88
+ if (!(error instanceof LaunchFenceError) || error.code !== 'already_armed')
89
+ throw error;
90
+ adopted = true;
91
+ }
92
+ grant = launchGrant(input.turn_id, cwd);
93
+ }
94
+ input.on_stage?.('armed');
95
+ if (!grant || (grant.status !== 'armed' && grant.status !== 'crossed')) {
96
+ throw new LaunchFenceError(input.turn_id, grant?.status === 'revoked' ? 'revoked' : 'not_armed', `prepareAttempt: launch generation for ${input.turn_id} is ${grant?.status ?? 'absent'}`);
97
+ }
98
+ return {
99
+ turn_id: input.turn_id,
100
+ reservation: { ...committed, launch: grant },
101
+ token: grant.token,
102
+ epoch: grant.epoch,
103
+ adopted: adopted || grant.status === 'crossed',
104
+ launch_status: grant.status,
105
+ };
106
+ }
107
+ /** Materialize deterministic projections and atomically cross the launch fence. */
108
+ export function projectAndCross(prepared, projectProjections, cwd, authorityActor = 'attempt-authority') {
109
+ const crossed = consumeLaunchGrantWithProjection(prepared.turn_id, prepared.token, prepared.epoch, projectProjections, cwd, authorityActor);
110
+ return {
111
+ kind: crossed.wonTransition ? 'won' : 'adopted',
112
+ reservation: crossed.reservation,
113
+ turn_id: prepared.turn_id,
114
+ token: prepared.token,
115
+ epoch: prepared.epoch,
116
+ };
117
+ }
118
+ export function inspectAttempt(turnId, cwd, runStatus) {
119
+ const reservation = getReservation(turnId, cwd);
120
+ if (!reservation)
121
+ return undefined;
122
+ return {
123
+ reservation,
124
+ status: attemptStatus(reservation, runStatus),
125
+ nonce: currentNonce(reservation),
126
+ };
127
+ }
128
+ export function matchEvidence(attempt, evidence) {
129
+ const reservation = 'reservation' in attempt ? attempt.reservation : attempt;
130
+ return evidenceMatchesAttempt(reservation, evidence);
131
+ }
132
+ /**
133
+ * Revoke a not-yet-crossed launch generation. A merely prepared reservation is
134
+ * aborted; a committed-but-unarmed reservation is left repairable by design.
135
+ */
136
+ export function revokeAttempt(turnId, reason, cwd, actor = 'attempt-authority') {
137
+ const reservation = getReservation(turnId, cwd);
138
+ if (!reservation) {
139
+ throw new ReservationStateError(turnId, 'reservation_not_found', `revokeAttempt: unknown turn_id ${turnId}`);
140
+ }
141
+ let next = reservation;
142
+ if (reservation.decision === 'prepared') {
143
+ next = abortReservation(turnId, reason, cwd, actor);
144
+ }
145
+ else if (reservation.launch?.status === 'armed') {
146
+ next = revokeLaunchGrant(turnId, reservation.launch.epoch, reason, cwd, actor);
147
+ }
148
+ else if (reservation.launch?.status === 'crossed') {
149
+ throw new LaunchFenceError(turnId, 'crossed_not_revocable', `revokeAttempt: ${turnId} already crossed`);
150
+ }
151
+ else if (!reservation.launch) {
152
+ throw new LaunchFenceError(turnId, 'not_armed', `revokeAttempt: ${turnId} has no launch generation`);
153
+ }
154
+ return { reservation: next, status: attemptStatus(next), nonce: currentNonce(next) };
155
+ }
156
+ /** Explicit prepared→aborted helper for recovery code that knows no commit occurred. */
157
+ export function abortAttempt(turnId, reason, cwd, actor = 'attempt-authority') {
158
+ const reservation = abortReservation(turnId, reason, cwd, actor);
159
+ return { reservation, status: attemptStatus(reservation), nonce: currentNonce(reservation) };
160
+ }
161
+ /* ======================= AttemptAuthority v2 ============================ */
162
+ export function attemptWorkspaceDigest(workspacePath, turnId, attemptEpoch) {
163
+ let resolved;
164
+ try {
165
+ resolved = fs.realpathSync.native(workspacePath);
166
+ }
167
+ catch {
168
+ resolved = path.resolve(workspacePath);
169
+ }
170
+ if (process.platform === 'win32')
171
+ resolved = resolved.toLowerCase();
172
+ return crypto.createHash('sha256')
173
+ .update(`${resolved}\0${turnId}\0${attemptEpoch}`, 'utf8')
174
+ .digest('hex');
175
+ }
176
+ function canonicalWorkspacePath(workspacePath) {
177
+ let resolved;
178
+ try {
179
+ resolved = fs.realpathSync.native(workspacePath);
180
+ }
181
+ catch {
182
+ resolved = path.resolve(workspacePath);
183
+ }
184
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
185
+ }
186
+ function gitWorkspaceIdentity(workspacePath) {
187
+ const result = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-dir', '--git-common-dir', '--show-toplevel'], { cwd: workspacePath, encoding: 'utf8', windowsHide: true });
188
+ const lines = result.status === 0 ? (result.stdout ?? '').trim().split(/\r?\n/) : [];
189
+ if (lines.length < 3) {
190
+ throw new AttemptGenerationError('invalid_transition', `workspace is not an attestable Git worktree: ${workspacePath}`);
191
+ }
192
+ return {
193
+ git_dir: canonicalWorkspacePath(lines[0]),
194
+ common_dir: canonicalWorkspacePath(lines[1]),
195
+ top_level: canonicalWorkspacePath(lines[2]),
196
+ };
197
+ }
198
+ export function executionContractForGeneration(reservation, generation) {
199
+ if (!reservation.execution_contract || !reservation.capability_snapshot) {
200
+ throw new AttemptGenerationError('invalid_transition', `turn ${reservation.turn_id} has no immutable execution contract`);
201
+ }
202
+ const contract = ExecutionContractSchema.parse({
203
+ ...reservation.execution_contract,
204
+ identity: {
205
+ ...reservation.execution_contract.identity,
206
+ logical_attempt_epoch: generation.attempt_epoch,
207
+ assignment_id: generation.assignment_id,
208
+ run_id: generation.run_id,
209
+ },
210
+ workspace_policy: {
211
+ ...reservation.execution_contract.workspace_policy,
212
+ cwd: generation.workspace_path,
213
+ worktree_path: generation.workspace_path,
214
+ isolation: 'worktree',
215
+ },
216
+ });
217
+ const ref = executionContractRef(contract, reservation.capability_snapshot);
218
+ if (ref.hash !== generation.contract_hash) {
219
+ throw new AttemptGenerationError('fenced', `generation ${generation.attempt_epoch} contract hash ${generation.contract_hash} does not match derived ${ref.hash}`);
220
+ }
221
+ return { contract, ref };
222
+ }
223
+ /**
224
+ * Anchor a crossed legacy generation into the v2 immutable chain. This is an
225
+ * explicit Release-B write and therefore refuses unless the signed writer
226
+ * guard is active on the local authority home.
227
+ */
228
+ export function bootstrapAttemptAuthorityV2(input) {
229
+ assertAttemptAuthorityV2Writable(input.cwd, input.authority_home, undefined, input.writer_id);
230
+ const reservation = getReservation(input.turn_id, input.cwd);
231
+ if (!reservation || reservation.decision !== 'committed') {
232
+ throw new AttemptGenerationError('invalid_transition', `turn ${input.turn_id} is not a committed reservation`);
233
+ }
234
+ const grant = launchGrant(input.turn_id, input.cwd);
235
+ if (!grant || grant.status !== 'crossed') {
236
+ throw new AttemptGenerationError('invalid_transition', `turn ${input.turn_id} is not a crossed generation`);
237
+ }
238
+ if (!reservation.execution_contract_ref) {
239
+ throw new AttemptGenerationError('invalid_transition', `turn ${input.turn_id} is legacy-uncontracted and cannot enter v2`);
240
+ }
241
+ const workspacePath = path.resolve(input.workspace_path
242
+ ?? reservation.execution_contract?.workspace_policy.worktree_path
243
+ ?? reservation.cwd);
244
+ const existingAnchor = readInitialGeneration(input.cwd, input.turn_id);
245
+ const initial = existingAnchor ?? prepareInitialGeneration({
246
+ turn_id: reservation.turn_id,
247
+ authority_home: input.authority_home,
248
+ contract_hash: reservation.execution_contract_ref.hash,
249
+ workspace_path: workspacePath,
250
+ workspace_digest: attemptWorkspaceDigest(workspacePath, reservation.turn_id, 0),
251
+ launch_nonce: grant.token,
252
+ });
253
+ if (initial.assignment_id !== reservation.child_ids.assignment_id
254
+ || initial.run_id !== reservation.child_ids.run_id
255
+ || initial.launch_nonce !== grant.token
256
+ || initial.contract_hash !== reservation.execution_contract_ref.hash
257
+ || canonicalWorkspacePath(initial.workspace_path) !== canonicalWorkspacePath(workspacePath)
258
+ || initial.workspace_digest !== attemptWorkspaceDigest(workspacePath, reservation.turn_id, 0)
259
+ || JSON.stringify(initial.authority_home) !== JSON.stringify(input.authority_home)) {
260
+ throw new AttemptGenerationError('invalid_transition', 'generation-zero anchor diverges from the existing reservation');
261
+ }
262
+ const anchored = publishInitialGeneration(input.cwd, initial).cell;
263
+ input.on_stage?.('initial_anchored');
264
+ const launch = prepareLaunchDecision(anchored, {
265
+ decision: 'crossed',
266
+ actor: input.actor,
267
+ cause: 'release-b bootstrap of already-crossed generation',
268
+ });
269
+ const publishedLaunch = publishPreparedLaunchDecision(input.cwd, anchored, launch).cell;
270
+ if (publishedLaunch.decision !== 'crossed') {
271
+ throw new AttemptGenerationError('fenced', `generation zero was already revoked for ${input.turn_id}`);
272
+ }
273
+ input.on_stage?.('launch_crossed');
274
+ rebuildAttemptGenerationHead(input.cwd, anchored);
275
+ return anchored;
276
+ }
277
+ /**
278
+ * Close the active epoch and activate its successor with ONE close(epoch) CAS.
279
+ * No projection is authoritative: callers may replay AgentRun/head/event
280
+ * materialization after this function returns.
281
+ */
282
+ export function prepareAttemptTakeoverV2(input) {
283
+ const rollout = assertAttemptAuthorityV2Writable(input.cwd, input.authority_home, undefined, input.writer_id);
284
+ if (input.external_effect_policy !== 'none'
285
+ && input.external_effect_policy !== 'idempotent'
286
+ && input.external_effect_policy !== 'externally_fenced') {
287
+ throw new AttemptGenerationError('invalid_transition', 'automatic takeover requires idempotent or externally fenced effects');
288
+ }
289
+ if (!input.cause.trim() || !input.liveness_evidence.trim()) {
290
+ throw new AttemptGenerationError('invalid_transition', 'takeover requires a cause and liveness evidence');
291
+ }
292
+ const reservation = getReservation(input.turn_id, input.cwd);
293
+ const resolved = resolveTurnGenerationChain(input.cwd, input.turn_id);
294
+ if (!reservation || !resolved) {
295
+ throw new AttemptGenerationError('invalid_transition', `turn ${input.turn_id} has no active v2 generation`);
296
+ }
297
+ const requestedCause = `${input.cause}; liveness=${input.liveness_evidence}; effects=${input.external_effect_policy}`;
298
+ // Crash replay: close(epoch) may have won before the LoopEvent/AgentRun/head
299
+ // projections were written. Adopt that exact immutable successor so the
300
+ // caller can repair projections; never mint another generation on replay.
301
+ if (resolved.latest_generation.attempt_epoch > input.expected_epoch) {
302
+ const initial = readInitialGeneration(input.cwd, input.turn_id);
303
+ const previous = initial
304
+ ? listAttemptGenerations(input.cwd, initial).find((generation) => generation.attempt_epoch === input.expected_epoch)
305
+ : undefined;
306
+ const close = previous ? readCloseDecision(input.cwd, input.turn_id, input.expected_epoch) : undefined;
307
+ const expectedMode = input.mode ?? 'takeover';
308
+ if (previous
309
+ && close
310
+ && (close.decision === 'takeover' || close.decision === 'retry')
311
+ && close.decision === expectedMode
312
+ && close.cause === requestedCause
313
+ && canonicalWorkspacePath(close.next_generation.workspace_path) === canonicalWorkspacePath(input.next_workspace_path)
314
+ && JSON.stringify(close.next_generation.authority_home) === JSON.stringify(input.authority_home)) {
315
+ const generationContract = executionContractForGeneration(reservation, close.next_generation);
316
+ return {
317
+ won: false,
318
+ rollout,
319
+ previous_generation: previous,
320
+ next_generation: close.next_generation,
321
+ close_cell: close,
322
+ execution_contract: generationContract.contract,
323
+ execution_contract_ref: generationContract.ref,
324
+ };
325
+ }
326
+ throw new AttemptGenerationError('fenced', `expected epoch ${input.expected_epoch}, active epoch is ${resolved.latest_generation.attempt_epoch}`);
327
+ }
328
+ if (resolved.status !== 'active') {
329
+ throw new AttemptGenerationError('invalid_transition', `turn ${input.turn_id} has no active v2 generation`);
330
+ }
331
+ const current = resolved.latest_generation;
332
+ if (current.attempt_epoch !== input.expected_epoch) {
333
+ throw new AttemptGenerationError('fenced', `expected epoch ${input.expected_epoch}, active epoch is ${current.attempt_epoch}`);
334
+ }
335
+ if (JSON.stringify(current.authority_home) !== JSON.stringify(input.authority_home)) {
336
+ throw new AttemptGenerationError('authority_home_mismatch', 'takeover caller is not authority_home');
337
+ }
338
+ const currentLaunch = readLaunchDecision(input.cwd, input.turn_id, current.attempt_epoch);
339
+ if (!currentLaunch || currentLaunch.decision !== 'crossed') {
340
+ throw new AttemptGenerationError('invalid_transition', `generation ${current.attempt_epoch} never crossed`);
341
+ }
342
+ const nextEpoch = current.attempt_epoch + 1;
343
+ const nextWorkspacePath = path.resolve(input.next_workspace_path);
344
+ if (canonicalWorkspacePath(nextWorkspacePath) === canonicalWorkspacePath(current.workspace_path)) {
345
+ throw new AttemptGenerationError('invalid_transition', 'next generation workspace resolves to the current workspace (alias/junction reuse refused)');
346
+ }
347
+ const currentWorkspace = gitWorkspaceIdentity(current.workspace_path);
348
+ const nextWorkspace = gitWorkspaceIdentity(nextWorkspacePath);
349
+ if (currentWorkspace.common_dir !== nextWorkspace.common_dir) {
350
+ throw new AttemptGenerationError('invalid_transition', 'next generation workspace is not a worktree of the same Git repository');
351
+ }
352
+ if (currentWorkspace.git_dir === nextWorkspace.git_dir || currentWorkspace.top_level === nextWorkspace.top_level) {
353
+ throw new AttemptGenerationError('invalid_transition', 'next generation must use a distinct Git worktree/gitdir');
354
+ }
355
+ const provisional = prepareNextGeneration(current, {
356
+ contract_hash: '0'.repeat(64),
357
+ workspace_path: nextWorkspacePath,
358
+ workspace_digest: attemptWorkspaceDigest(nextWorkspacePath, input.turn_id, nextEpoch),
359
+ });
360
+ const baseContract = reservation.execution_contract;
361
+ const snapshot = reservation.capability_snapshot;
362
+ if (!baseContract || !snapshot) {
363
+ throw new AttemptGenerationError('invalid_transition', `turn ${input.turn_id} lacks a contracted capability snapshot`);
364
+ }
365
+ const nextContract = ExecutionContractSchema.parse({
366
+ ...baseContract,
367
+ identity: {
368
+ ...baseContract.identity,
369
+ logical_attempt_epoch: nextEpoch,
370
+ assignment_id: provisional.assignment_id,
371
+ run_id: provisional.run_id,
372
+ },
373
+ workspace_policy: {
374
+ ...baseContract.workspace_policy,
375
+ cwd: nextWorkspacePath,
376
+ worktree_path: nextWorkspacePath,
377
+ isolation: 'worktree',
378
+ },
379
+ });
380
+ const nextRef = executionContractRef(nextContract, snapshot);
381
+ const next = prepareNextGeneration(current, {
382
+ contract_hash: nextRef.hash,
383
+ workspace_path: nextWorkspacePath,
384
+ workspace_digest: attemptWorkspaceDigest(nextWorkspacePath, input.turn_id, nextEpoch),
385
+ launch_nonce: provisional.launch_nonce,
386
+ created_at: provisional.created_at,
387
+ });
388
+ const proposed = prepareCloseDecision(current, {
389
+ decision: input.mode ?? 'takeover',
390
+ actor: input.actor,
391
+ cause: requestedCause,
392
+ next_generation: next,
393
+ });
394
+ const published = publishPreparedCloseDecision(input.cwd, current, proposed);
395
+ const incumbent = published.cell;
396
+ if (incumbent.decision !== 'takeover' && incumbent.decision !== 'retry') {
397
+ throw new AttemptGenerationError('fenced', `generation ${current.attempt_epoch} already closed as ${incumbent.decision}`);
398
+ }
399
+ if (generationDigest(incumbent.next_generation) !== generationDigest(next)) {
400
+ throw new AttemptGenerationError('fenced', `a different successor already won generation ${current.attempt_epoch}`);
401
+ }
402
+ rebuildAttemptGenerationHead(input.cwd, current);
403
+ return {
404
+ won: published.won,
405
+ rollout,
406
+ previous_generation: current,
407
+ next_generation: incumbent.next_generation,
408
+ close_cell: incumbent,
409
+ execution_contract: nextContract,
410
+ execution_contract_ref: nextRef,
411
+ };
412
+ }
413
+ export function crossActiveAttemptGenerationV2(turnId, expectedEpoch, authorityHome, actor, writerId, cwd) {
414
+ assertAttemptAuthorityV2Writable(cwd, authorityHome, undefined, writerId);
415
+ const resolved = resolveTurnGenerationChain(cwd, turnId);
416
+ if (!resolved || resolved.status !== 'active' || resolved.latest_generation.attempt_epoch !== expectedEpoch) {
417
+ throw new AttemptGenerationError('fenced', `turn ${turnId} epoch ${expectedEpoch} is not active`);
418
+ }
419
+ const generation = resolved.latest_generation;
420
+ if (JSON.stringify(generation.authority_home) !== JSON.stringify(authorityHome)) {
421
+ throw new AttemptGenerationError('authority_home_mismatch', 'cross caller is not authority_home');
422
+ }
423
+ const published = publishPreparedLaunchDecision(cwd, generation, prepareLaunchDecision(generation, {
424
+ decision: 'crossed', actor, cause: 'projection-complete launch crossing',
425
+ }));
426
+ if (published.cell.decision !== 'crossed') {
427
+ throw new AttemptGenerationError('fenced', `generation ${expectedEpoch} was revoked before crossing`);
428
+ }
429
+ return { won: published.won, generation, fence: fenceForGeneration(generation) };
430
+ }
431
+ export function settleActiveAttemptGenerationV2(turnId, expectedFence, result, authorityHome, actor, writerId, cwd) {
432
+ assertAttemptAuthorityV2Writable(cwd, authorityHome, undefined, writerId);
433
+ const resolved = resolveTurnGenerationChain(cwd, turnId);
434
+ if (!resolved)
435
+ return undefined;
436
+ if (resolved.status !== 'active') {
437
+ const terminal = resolved.terminal_cell;
438
+ if (!terminal || terminal.decision !== 'settled' || !terminal.result_digest)
439
+ return undefined;
440
+ if (JSON.stringify(terminal.fence) !== JSON.stringify(expectedFence)) {
441
+ throw new AttemptGenerationError('fenced', `settled generation fence does not match ${turnId}`);
442
+ }
443
+ const evidence = readAttemptResultEvidence(cwd, turnId, terminal.fence.attempt_epoch, terminal.result_digest);
444
+ if (!evidence)
445
+ throw new AttemptGenerationError('fenced', `settled evidence ${terminal.result_digest} is missing`);
446
+ return { won: false, cell: terminal, evidence };
447
+ }
448
+ const generation = resolved.latest_generation;
449
+ if (JSON.stringify(fenceForGeneration(generation)) !== JSON.stringify(expectedFence)) {
450
+ throw new AttemptGenerationError('fenced', 'settlement fence no longer matches the active generation');
451
+ }
452
+ if (JSON.stringify(generation.authority_home) !== JSON.stringify(authorityHome)) {
453
+ throw new AttemptGenerationError('authority_home_mismatch', 'settlement caller is not authority_home');
454
+ }
455
+ const launch = readLaunchDecision(cwd, turnId, generation.attempt_epoch);
456
+ if (!launch || launch.decision !== 'crossed') {
457
+ throw new AttemptGenerationError('invalid_transition', `generation ${generation.attempt_epoch} never crossed`);
458
+ }
459
+ const publishedEvidence = publishAttemptResultEvidence(cwd, generation, result);
460
+ const proposed = prepareCloseDecision(generation, {
461
+ decision: 'settled', actor, cause: 'turn evidence accepted', result_digest: publishedEvidence.digest,
462
+ });
463
+ const published = publishPreparedCloseDecision(cwd, generation, proposed);
464
+ if (published.cell.decision !== 'settled') {
465
+ return { ...published, evidence: publishedEvidence.cell };
466
+ }
467
+ if (published.cell.result_digest !== publishedEvidence.digest) {
468
+ throw new AttemptGenerationError('fenced', `generation ${generation.attempt_epoch} already settled with different evidence`);
469
+ }
470
+ const authoritativeEvidence = readAttemptResultEvidence(cwd, turnId, generation.attempt_epoch, published.cell.result_digest);
471
+ if (!authoritativeEvidence)
472
+ throw new AttemptGenerationError('fenced', 'authoritative settlement evidence is missing');
473
+ rebuildAttemptGenerationHead(cwd, generation);
474
+ return { ...published, evidence: authoritativeEvidence };
475
+ }
476
+ //# sourceMappingURL=attempt-authority.js.map