@aviaratech/ai-delivery 0.3.15 → 0.3.17

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.
@@ -1,3 +1,4 @@
1
+ import { z } from 'zod';
1
2
  import type { WorktreeTransitionPlan } from '../worktreeTransition.js';
2
3
  export interface WorktreeEntry {
3
4
  branch: string;
@@ -12,10 +13,280 @@ export interface WorktreeEntry {
12
13
  updatedAt: string;
13
14
  }
14
15
  export type WorktreeStatus = 'active' | 'error' | 'merged' | 'pr-published' | 'stale';
16
+ /** @internal Private exact-plan checkpoint; it is not a public transition purpose. */
17
+ export declare const CommittedContinuationPlanSchema: z.ZodObject<{
18
+ schemaVersion: z.ZodLiteral<"ai-delivery.committed-continuation-plan@1">;
19
+ planId: z.ZodString;
20
+ repository: z.ZodString;
21
+ repoRoot: z.ZodString;
22
+ configDigest: z.ZodString;
23
+ currentRuntime: z.ZodObject<{
24
+ admissionId: z.ZodString;
25
+ capability: z.ZodObject<{
26
+ cli: z.ZodNumber;
27
+ mcp: z.ZodNumber;
28
+ }, z.core.$strict>;
29
+ cliPath: z.ZodString;
30
+ cliSha256: z.ZodString;
31
+ configDigest: z.ZodString;
32
+ mcpLauncherPath: z.ZodString;
33
+ mcpLauncherSha256: z.ZodString;
34
+ packageDistSha256: z.ZodString;
35
+ packageManifestSha256: z.ZodString;
36
+ packageVersion: z.ZodString;
37
+ pluginManifestPath: z.ZodString;
38
+ pluginManifestSha256: z.ZodString;
39
+ repository: z.ZodString;
40
+ schemaVersion: z.ZodLiteral<"ai-delivery.runtime-admission@2">;
41
+ sourceArchiveSha256: z.ZodString;
42
+ sourceCommit: z.ZodString;
43
+ }, z.core.$strict>;
44
+ row: z.ZodPipe<z.ZodObject<{
45
+ branch: z.ZodString;
46
+ createdAt: z.ZodString;
47
+ identity: z.ZodString;
48
+ issueNumber: z.ZodNumber;
49
+ path: z.ZodString;
50
+ prNumber: z.ZodOptional<z.ZodNumber>;
51
+ status: z.ZodEnum<{
52
+ error: "error";
53
+ active: "active";
54
+ stale: "stale";
55
+ merged: "merged";
56
+ "pr-published": "pr-published";
57
+ }>;
58
+ type: z.ZodLiteral<"issue">;
59
+ updatedAt: z.ZodString;
60
+ }, z.core.$strict>, z.ZodTransform<{
61
+ prNumber?: number;
62
+ branch: string;
63
+ createdAt: string;
64
+ identity: string;
65
+ issueNumber: number;
66
+ path: string;
67
+ status: "error" | "active" | "stale" | "merged" | "pr-published";
68
+ type: "issue";
69
+ updatedAt: string;
70
+ }, {
71
+ branch: string;
72
+ createdAt: string;
73
+ identity: string;
74
+ issueNumber: number;
75
+ path: string;
76
+ status: "error" | "active" | "stale" | "merged" | "pr-published";
77
+ type: "issue";
78
+ updatedAt: string;
79
+ prNumber?: number | undefined;
80
+ }>>;
81
+ replacement: z.ZodPipe<z.ZodObject<{
82
+ branch: z.ZodString;
83
+ createdAt: z.ZodString;
84
+ identity: z.ZodString;
85
+ issueNumber: z.ZodNumber;
86
+ path: z.ZodString;
87
+ prNumber: z.ZodOptional<z.ZodNumber>;
88
+ status: z.ZodEnum<{
89
+ error: "error";
90
+ active: "active";
91
+ stale: "stale";
92
+ merged: "merged";
93
+ "pr-published": "pr-published";
94
+ }>;
95
+ type: z.ZodLiteral<"issue">;
96
+ updatedAt: z.ZodString;
97
+ }, z.core.$strict>, z.ZodTransform<{
98
+ prNumber?: number;
99
+ branch: string;
100
+ createdAt: string;
101
+ identity: string;
102
+ issueNumber: number;
103
+ path: string;
104
+ status: "error" | "active" | "stale" | "merged" | "pr-published";
105
+ type: "issue";
106
+ updatedAt: string;
107
+ }, {
108
+ branch: string;
109
+ createdAt: string;
110
+ identity: string;
111
+ issueNumber: number;
112
+ path: string;
113
+ status: "error" | "active" | "stale" | "merged" | "pr-published";
114
+ type: "issue";
115
+ updatedAt: string;
116
+ prNumber?: number | undefined;
117
+ }>>;
118
+ head: z.ZodObject<{
119
+ sha: z.ZodString;
120
+ tree: z.ZodString;
121
+ }, z.core.$strict>;
122
+ terminalHead: z.ZodObject<{
123
+ sha: z.ZodString;
124
+ tree: z.ZodString;
125
+ }, z.core.$strict>;
126
+ terminalMergeId: z.ZodString;
127
+ operator: z.ZodObject<{
128
+ actorLogin: z.ZodString;
129
+ credentialIdentity: z.ZodString;
130
+ }, z.core.$strict>;
131
+ reviewerActor: z.ZodString;
132
+ preserved: z.ZodArray<z.ZodObject<{
133
+ path: z.ZodString;
134
+ digest: z.ZodString;
135
+ }, z.core.$strict>>;
136
+ }, z.core.$strict>;
137
+ export type CommittedContinuationPlan = z.infer<typeof CommittedContinuationPlanSchema>;
138
+ declare const ContinuationIntentSchema: z.ZodObject<{
139
+ schemaVersion: z.ZodLiteral<"ai-delivery.committed-continuation-intent@1">;
140
+ intentId: z.ZodString;
141
+ plan: z.ZodObject<{
142
+ schemaVersion: z.ZodLiteral<"ai-delivery.committed-continuation-plan@1">;
143
+ planId: z.ZodString;
144
+ repository: z.ZodString;
145
+ repoRoot: z.ZodString;
146
+ configDigest: z.ZodString;
147
+ currentRuntime: z.ZodObject<{
148
+ admissionId: z.ZodString;
149
+ capability: z.ZodObject<{
150
+ cli: z.ZodNumber;
151
+ mcp: z.ZodNumber;
152
+ }, z.core.$strict>;
153
+ cliPath: z.ZodString;
154
+ cliSha256: z.ZodString;
155
+ configDigest: z.ZodString;
156
+ mcpLauncherPath: z.ZodString;
157
+ mcpLauncherSha256: z.ZodString;
158
+ packageDistSha256: z.ZodString;
159
+ packageManifestSha256: z.ZodString;
160
+ packageVersion: z.ZodString;
161
+ pluginManifestPath: z.ZodString;
162
+ pluginManifestSha256: z.ZodString;
163
+ repository: z.ZodString;
164
+ schemaVersion: z.ZodLiteral<"ai-delivery.runtime-admission@2">;
165
+ sourceArchiveSha256: z.ZodString;
166
+ sourceCommit: z.ZodString;
167
+ }, z.core.$strict>;
168
+ row: z.ZodPipe<z.ZodObject<{
169
+ branch: z.ZodString;
170
+ createdAt: z.ZodString;
171
+ identity: z.ZodString;
172
+ issueNumber: z.ZodNumber;
173
+ path: z.ZodString;
174
+ prNumber: z.ZodOptional<z.ZodNumber>;
175
+ status: z.ZodEnum<{
176
+ error: "error";
177
+ active: "active";
178
+ stale: "stale";
179
+ merged: "merged";
180
+ "pr-published": "pr-published";
181
+ }>;
182
+ type: z.ZodLiteral<"issue">;
183
+ updatedAt: z.ZodString;
184
+ }, z.core.$strict>, z.ZodTransform<{
185
+ prNumber?: number;
186
+ branch: string;
187
+ createdAt: string;
188
+ identity: string;
189
+ issueNumber: number;
190
+ path: string;
191
+ status: "error" | "active" | "stale" | "merged" | "pr-published";
192
+ type: "issue";
193
+ updatedAt: string;
194
+ }, {
195
+ branch: string;
196
+ createdAt: string;
197
+ identity: string;
198
+ issueNumber: number;
199
+ path: string;
200
+ status: "error" | "active" | "stale" | "merged" | "pr-published";
201
+ type: "issue";
202
+ updatedAt: string;
203
+ prNumber?: number | undefined;
204
+ }>>;
205
+ replacement: z.ZodPipe<z.ZodObject<{
206
+ branch: z.ZodString;
207
+ createdAt: z.ZodString;
208
+ identity: z.ZodString;
209
+ issueNumber: z.ZodNumber;
210
+ path: z.ZodString;
211
+ prNumber: z.ZodOptional<z.ZodNumber>;
212
+ status: z.ZodEnum<{
213
+ error: "error";
214
+ active: "active";
215
+ stale: "stale";
216
+ merged: "merged";
217
+ "pr-published": "pr-published";
218
+ }>;
219
+ type: z.ZodLiteral<"issue">;
220
+ updatedAt: z.ZodString;
221
+ }, z.core.$strict>, z.ZodTransform<{
222
+ prNumber?: number;
223
+ branch: string;
224
+ createdAt: string;
225
+ identity: string;
226
+ issueNumber: number;
227
+ path: string;
228
+ status: "error" | "active" | "stale" | "merged" | "pr-published";
229
+ type: "issue";
230
+ updatedAt: string;
231
+ }, {
232
+ branch: string;
233
+ createdAt: string;
234
+ identity: string;
235
+ issueNumber: number;
236
+ path: string;
237
+ status: "error" | "active" | "stale" | "merged" | "pr-published";
238
+ type: "issue";
239
+ updatedAt: string;
240
+ prNumber?: number | undefined;
241
+ }>>;
242
+ head: z.ZodObject<{
243
+ sha: z.ZodString;
244
+ tree: z.ZodString;
245
+ }, z.core.$strict>;
246
+ terminalHead: z.ZodObject<{
247
+ sha: z.ZodString;
248
+ tree: z.ZodString;
249
+ }, z.core.$strict>;
250
+ terminalMergeId: z.ZodString;
251
+ operator: z.ZodObject<{
252
+ actorLogin: z.ZodString;
253
+ credentialIdentity: z.ZodString;
254
+ }, z.core.$strict>;
255
+ reviewerActor: z.ZodString;
256
+ preserved: z.ZodArray<z.ZodObject<{
257
+ path: z.ZodString;
258
+ digest: z.ZodString;
259
+ }, z.core.$strict>>;
260
+ }, z.core.$strict>;
261
+ authorityCommentId: z.ZodNumber;
262
+ acceptanceCommentId: z.ZodNumber;
263
+ }, z.core.$strict>;
264
+ type ContinuationIntent = z.infer<typeof ContinuationIntentSchema>;
265
+ export declare function committedContinuationPlanPath(root: string, issueNumber: number, head: string): string;
266
+ /** @internal Pending intents are recoverable only by the same supported develop boundary. */
267
+ export declare function pendingCommittedContinuation(issueNumber: number, root: string): ContinuationIntent | undefined;
15
268
  /** An immutable witness separates new rows from legacy rows in the same canonical registry. */
16
269
  export declare function assertAiDeliveryWorktreeOwner(entry: WorktreeEntry, root: string): void;
17
270
  /** Ordinary source mutations also refuse terminal/pending issue intent after the row was removed. */
18
- export declare function assertIssueWorktreeTransitionAdmission(issueNumber: number, root: string): void;
271
+ export declare function assertIssueWorktreeTransitionAdmission(issueNumber: number, root: string, committedRecovery?: boolean): void;
272
+ /** @internal New continuation checkpoints are published only while both owning locks are held. */
273
+ export declare function writeCommittedContinuationCheckpoint(path: string, value: unknown): void;
274
+ /** @internal One canonical row, its immutable witnesses, and the existing registry lock own this recovery. */
275
+ export declare function withCommittedContinuationRegistry<T>(root: string, issueNumber: number, operation: (input: {
276
+ current: WorktreeEntry;
277
+ pending: ContinuationIntent | undefined;
278
+ ownerWitness: {
279
+ path: string;
280
+ digest: string;
281
+ };
282
+ prepare(this: void): void;
283
+ begin(this: void, plan: CommittedContinuationPlan, ids: {
284
+ authorityCommentId: number;
285
+ acceptanceCommentId: number;
286
+ }): void;
287
+ commit(this: void): void;
288
+ complete(this: void): void;
289
+ }) => Promise<T>): Promise<T>;
19
290
  /** @internal The explicit transition owner holds the existing registry lock through its exact planned mutation. */
20
291
  export declare function withWorktreeTransitionRegistry<T>(plan: WorktreeTransitionPlan, operation: (input: {
21
292
  current: WorktreeEntry | undefined;
@@ -1,14 +1,152 @@
1
- import { existsSync, lstatSync, readFileSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
3
- import { assertPrivateFile, stableJson, writeCreateOnly } from '../delivery/common.js';
1
+ import { closeSync, existsSync, fsyncSync, lstatSync, openSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { assertPrivateFile, CoordinateSchema, DigestSchema, digestBytes, stableJson, writeCreateOnly, } from '../delivery/common.js';
4
5
  import { digestValue } from '../delivery/index.js';
5
6
  import { gitCommonDir } from '../git.js';
7
+ import { RuntimeAdmissionSchema } from './deliveryAdmission.js';
6
8
  import { logInfo, logWarn } from '../logger.js';
7
9
  import { ensureDataDirectory, ISSUE_CLI_DATA_DIR } from '../utils/filesystem.js';
8
- import { writeJsonFileAtomically } from '../utils/atomicJson.js';
10
+ import { ensurePrivateDirectoryDurably, writeJsonFileAtomically, writePrivateJsonFileAtomically, } from '../utils/atomicJson.js';
9
11
  import { withLock } from '../utils/lockfile.js';
10
12
  const REGISTRY_PATH = `${ISSUE_CLI_DATA_DIR}/worktrees.json`;
11
13
  const LOCK_RETRY_MESSAGE = 'Worktree registry locked by another process. Retrying...';
14
+ const ContinuationRowSchema = z
15
+ .strictObject({
16
+ branch: z.string().min(1),
17
+ createdAt: z.string().min(1),
18
+ identity: z.string().min(1),
19
+ issueNumber: z.number().int().positive(),
20
+ path: z.string().min(1),
21
+ prNumber: z.number().int().positive().optional(),
22
+ status: z.enum(['active', 'error', 'merged', 'pr-published', 'stale']),
23
+ type: z.literal('issue'),
24
+ updatedAt: z.string().min(1),
25
+ })
26
+ .transform(({ prNumber, ...row }) => ({ ...row, ...(prNumber === undefined ? {} : { prNumber }) }));
27
+ /** @internal Private exact-plan checkpoint; it is not a public transition purpose. */
28
+ export const CommittedContinuationPlanSchema = z
29
+ .strictObject({
30
+ schemaVersion: z.literal('ai-delivery.committed-continuation-plan@1'),
31
+ planId: DigestSchema,
32
+ repository: z.string().min(1),
33
+ repoRoot: z.string().min(1),
34
+ configDigest: DigestSchema,
35
+ currentRuntime: RuntimeAdmissionSchema,
36
+ row: ContinuationRowSchema,
37
+ replacement: ContinuationRowSchema,
38
+ head: CoordinateSchema,
39
+ terminalHead: CoordinateSchema,
40
+ terminalMergeId: DigestSchema,
41
+ operator: z.strictObject({
42
+ actorLogin: z.string().min(1),
43
+ credentialIdentity: z.string().regex(/^user:[1-9]\d*$/u),
44
+ }),
45
+ reviewerActor: z.string().min(1),
46
+ preserved: z.array(z.strictObject({ path: z.string().min(1), digest: DigestSchema })).min(1),
47
+ })
48
+ .superRefine((plan, ctx) => {
49
+ const { planId, ...content } = plan;
50
+ const { prNumber: _pr, identity: _identity, status: _status, updatedAt: _updated, ...original } = plan.row;
51
+ const { identity: _newIdentity, status: _newStatus, updatedAt: _newUpdated, ...replacement } = plan.replacement;
52
+ if (planId !== digestValue(content) ||
53
+ plan.row.status !== 'merged' ||
54
+ !plan.row.prNumber ||
55
+ plan.replacement.status !== 'active' ||
56
+ plan.replacement.prNumber !== undefined ||
57
+ stableJson(original) !== stableJson(replacement) ||
58
+ plan.head.sha === plan.terminalHead.sha ||
59
+ new Set(plan.preserved.map((file) => file.path)).size !== plan.preserved.length)
60
+ ctx.addIssue({ code: 'custom', message: 'Committed continuation plan identity or disposition is invalid.' });
61
+ });
62
+ const ContinuationIntentSchema = z
63
+ .strictObject({
64
+ schemaVersion: z.literal('ai-delivery.committed-continuation-intent@1'),
65
+ intentId: DigestSchema,
66
+ plan: CommittedContinuationPlanSchema,
67
+ authorityCommentId: z.number().int().positive(),
68
+ acceptanceCommentId: z.number().int().positive(),
69
+ })
70
+ .superRefine(({ intentId, ...content }, ctx) => {
71
+ if (intentId !== digestValue(content))
72
+ ctx.addIssue({ code: 'custom', message: 'Continuation intent identity is invalid.' });
73
+ });
74
+ const ContinuationCompletionSchema = z
75
+ .strictObject({
76
+ schemaVersion: z.literal('ai-delivery.committed-continuation-completion@1'),
77
+ completionId: DigestSchema,
78
+ intentId: DigestSchema,
79
+ planId: DigestSchema,
80
+ row: ContinuationRowSchema,
81
+ head: CoordinateSchema,
82
+ completed: z.literal(true),
83
+ })
84
+ .superRefine(({ completionId, ...content }, ctx) => {
85
+ if (completionId !== digestValue(content))
86
+ ctx.addIssue({ code: 'custom', message: 'Continuation completion identity is invalid.' });
87
+ });
88
+ export function committedContinuationPlanPath(root, issueNumber, head) {
89
+ return join(gitCommonDir(root), 'ai-delivery', 'continuations', String(issueNumber), `${head}.plan.json`);
90
+ }
91
+ function continuationIntentPath(plan) {
92
+ return committedContinuationPlanPath(plan.repoRoot, plan.row.issueNumber, plan.head.sha).replace(/\.plan\.json$/u, '.intent.json');
93
+ }
94
+ function continuationCompletionPath(plan) {
95
+ return continuationIntentPath(plan).replace(/\.intent\.json$/u, '.completion.json');
96
+ }
97
+ function continuationBeforeimagePath(plan, digest) {
98
+ return join(gitCommonDir(plan.repoRoot), 'ai-delivery', 'continuations', 'evidence', `${digest.slice(7)}.bin`);
99
+ }
100
+ function assertContinuationPreserved(plan, beforeimages = false) {
101
+ for (const file of plan.preserved) {
102
+ if (digestBytes(assertPrivateFile(file.path)) !== file.digest ||
103
+ (beforeimages && digestBytes(assertPrivateFile(continuationBeforeimagePath(plan, file.digest))) !== file.digest))
104
+ throw new Error('Committed continuation original evidence changed.');
105
+ }
106
+ if (beforeimages) {
107
+ const bytes = Buffer.from(stableJson(plan.row));
108
+ if (!assertPrivateFile(continuationBeforeimagePath(plan, digestBytes(bytes))).equals(bytes))
109
+ throw new Error('Committed continuation original row beforeimage changed.');
110
+ }
111
+ }
112
+ /** @internal Pending intents are recoverable only by the same supported develop boundary. */
113
+ export function pendingCommittedContinuation(issueNumber, root) {
114
+ const directory = join(gitCommonDir(root), 'ai-delivery', 'continuations', String(issueNumber));
115
+ if (!existsSync(directory))
116
+ return undefined;
117
+ let pending;
118
+ for (const name of readdirSync(directory)
119
+ .filter((name) => name.endsWith('.intent.json'))
120
+ .sort()) {
121
+ const intent = ContinuationIntentSchema.parse(JSON.parse(assertPrivateFile(join(directory, name)).toString('utf8')));
122
+ const plan = intent.plan;
123
+ if (plan.repoRoot !== resolve(root) ||
124
+ plan.row.issueNumber !== issueNumber ||
125
+ join(directory, name) !== continuationIntentPath(plan))
126
+ throw new Error('Committed continuation checkpoint identity disagrees.');
127
+ assertContinuationPreserved(plan, true);
128
+ const completionPath = continuationCompletionPath(plan);
129
+ if (existsSync(completionPath)) {
130
+ const completion = ContinuationCompletionSchema.parse(JSON.parse(assertPrivateFile(completionPath).toString('utf8')));
131
+ if (completion.intentId !== intent.intentId ||
132
+ completion.planId !== plan.planId ||
133
+ stableJson(completion.row) !== stableJson(plan.replacement) ||
134
+ stableJson(completion.head) !== stableJson(plan.head))
135
+ throw new Error('Committed continuation completion disagrees with its intent.');
136
+ assertOwnerWitness(plan.replacement, root);
137
+ // Terminal recovery validates existing ancestors and repairs their directory durability.
138
+ prepareCommittedContinuationDirectory(root, directory, false);
139
+ prepareCommittedContinuationDirectory(root, dirname(continuationBeforeimagePath(plan, plan.preserved[0].digest)), false);
140
+ syncCommittedContinuationDirectory(completionPath);
141
+ }
142
+ else {
143
+ if (pending)
144
+ throw new Error('Issue has multiple incomplete committed continuations.');
145
+ pending = intent;
146
+ }
147
+ }
148
+ return pending;
149
+ }
12
150
  function ownerContent(entry) {
13
151
  if (!entry.identity)
14
152
  throw new Error('ai-delivery worktree ownership requires the preparing identity.');
@@ -71,6 +209,11 @@ function assertNoIncompleteWorktreeTransition(entry, root) {
71
209
  /** An immutable witness separates new rows from legacy rows in the same canonical registry. */
72
210
  export function assertAiDeliveryWorktreeOwner(entry, root) {
73
211
  assertNoIncompleteWorktreeTransition(entry, root);
212
+ if (entry.issueNumber !== undefined && pendingCommittedContinuation(entry.issueNumber, root))
213
+ throw new Error('Committed continuation is pending or incomplete; resume supported develop.');
214
+ assertOwnerWitness(entry, root);
215
+ }
216
+ function assertOwnerWitness(entry, root) {
74
217
  const content = ownerContent(entry);
75
218
  const path = ownerPath(root, entry);
76
219
  let stored;
@@ -85,7 +228,9 @@ export function assertAiDeliveryWorktreeOwner(entry, root) {
85
228
  }
86
229
  }
87
230
  /** Ordinary source mutations also refuse terminal/pending issue intent after the row was removed. */
88
- export function assertIssueWorktreeTransitionAdmission(issueNumber, root) {
231
+ export function assertIssueWorktreeTransitionAdmission(issueNumber, root, committedRecovery = false) {
232
+ if (pendingCommittedContinuation(issueNumber, root) && !committedRecovery)
233
+ throw new Error('Committed continuation is pending or incomplete; resume supported develop.');
89
234
  const path = join(gitCommonDir(root), 'ai-delivery', 'worktree-owners', `issue-${String(issueNumber)}.transition.json`);
90
235
  try {
91
236
  lstatSync(path);
@@ -112,9 +257,163 @@ function writeWorktreeOwner(entry, root, transition = false) {
112
257
  if (!transition)
113
258
  assertAiDeliveryWorktreeOwner(entry, root);
114
259
  }
260
+ /** @internal New continuation checkpoints are published only while both owning locks are held. */
261
+ export function writeCommittedContinuationCheckpoint(path, value) {
262
+ const assertEqual = () => {
263
+ const stored = JSON.parse(assertPrivateFile(path).toString('utf8'));
264
+ if (stableJson(stored) !== stableJson(value))
265
+ throw new Error('Committed continuation checkpoint changed.');
266
+ };
267
+ try {
268
+ assertEqual();
269
+ }
270
+ catch (error) {
271
+ if (error.code !== 'ENOENT')
272
+ throw error;
273
+ writePrivateJsonFileAtomically(path, value);
274
+ assertEqual();
275
+ }
276
+ // Retry completes durability if the prior publisher died after rename but before directory sync.
277
+ syncCommittedContinuationDirectory(path);
278
+ }
279
+ function syncCommittedContinuationDirectory(path) {
280
+ const directory = openSync(dirname(path), 'r');
281
+ try {
282
+ fsyncSync(directory);
283
+ }
284
+ finally {
285
+ closeSync(directory);
286
+ }
287
+ }
288
+ /** Repair only this new namespace, anchored at the existing Git common directory. */
289
+ function prepareCommittedContinuationDirectory(root, directory, create = true) {
290
+ const deliveryDirectory = join(gitCommonDir(root), 'ai-delivery');
291
+ const continuationRoot = join(deliveryDirectory, 'continuations');
292
+ const paths = [deliveryDirectory, continuationRoot, directory];
293
+ const validate = (path, missing) => {
294
+ let metadata;
295
+ try {
296
+ metadata = lstatSync(path);
297
+ }
298
+ catch (error) {
299
+ if (missing && error.code === 'ENOENT')
300
+ return;
301
+ throw error;
302
+ }
303
+ if (!metadata.isDirectory() ||
304
+ metadata.isSymbolicLink() ||
305
+ (path !== deliveryDirectory && (metadata.mode & 0o077) !== 0))
306
+ throw new Error('Committed continuation evidence requires a private directory.');
307
+ };
308
+ for (const path of paths)
309
+ validate(path, create && path !== deliveryDirectory);
310
+ if (create)
311
+ ensurePrivateDirectoryDurably(directory);
312
+ for (const path of paths) {
313
+ validate(path, false);
314
+ // Existing mkdir state can be recovery from death before its parent was synced.
315
+ syncCommittedContinuationDirectory(path);
316
+ }
317
+ }
318
+ /** @internal One canonical row, its immutable witnesses, and the existing registry lock own this recovery. */
319
+ export async function withCommittedContinuationRegistry(root, issueNumber, operation) {
320
+ ensureRegistryDirectory(root);
321
+ return withLock(REGISTRY_PATH, {
322
+ projectRoot: root,
323
+ timeout: 5000,
324
+ operation: async () => {
325
+ const pending = pendingCommittedContinuation(issueNumber, root);
326
+ let intent = pending;
327
+ let current;
328
+ const readCurrent = () => {
329
+ const registry = loadRegistryStrict(root);
330
+ const matches = registry.worktrees.filter((row) => row.type === 'issue' && row.issueNumber === issueNumber);
331
+ const row = matches[0];
332
+ if (matches.length !== 1 || !row)
333
+ throw new Error('Committed continuation requires one exact canonical issue row.');
334
+ if (registry.worktrees.some((other) => other !== row && registryIdentitiesOverlap(other, row)))
335
+ throw new Error('Committed continuation overlaps another canonical owner.');
336
+ assertNoIncompleteWorktreeTransition(row, root);
337
+ assertOwnerWitness(row, root);
338
+ return { registry, row };
339
+ };
340
+ current = readCurrent().row;
341
+ if (pending &&
342
+ ![pending.plan.row, pending.plan.replacement].some((row) => stableJson(row) === stableJson(current)))
343
+ throw new Error('Committed continuation canonical row drifted.');
344
+ const original = pending?.plan.row ?? current;
345
+ assertOwnerWitness(original, root);
346
+ const witnessPath = ownerPath(root, original);
347
+ const ownerWitness = { path: witnessPath, digest: digestBytes(assertPrivateFile(witnessPath)) };
348
+ return operation({
349
+ current,
350
+ pending,
351
+ ownerWitness,
352
+ prepare: () => prepareCommittedContinuationDirectory(root, join(gitCommonDir(root), 'ai-delivery', 'continuations', String(issueNumber))),
353
+ begin: (raw, ids) => {
354
+ const plan = CommittedContinuationPlanSchema.parse(raw);
355
+ if (plan.repoRoot !== resolve(root) ||
356
+ plan.row.issueNumber !== issueNumber ||
357
+ stableJson(plan.row) !== stableJson(original) ||
358
+ !plan.preserved.some((file) => stableJson(file) === stableJson(ownerWitness)))
359
+ throw new Error('Committed continuation lacks exact original custody.');
360
+ const content = { schemaVersion: 'ai-delivery.committed-continuation-intent@1', plan, ...ids };
361
+ const next = ContinuationIntentSchema.parse({ ...content, intentId: digestValue(content) });
362
+ if (intent && stableJson(next) !== stableJson(intent))
363
+ throw new Error('Committed continuation intent drifted.');
364
+ assertContinuationPreserved(plan);
365
+ const evidenceDirectory = dirname(continuationBeforeimagePath(plan, plan.preserved[0].digest));
366
+ prepareCommittedContinuationDirectory(root, evidenceDirectory);
367
+ for (const file of plan.preserved)
368
+ writeCreateOnly(continuationBeforeimagePath(plan, file.digest), assertPrivateFile(file.path), file.digest);
369
+ const rowBytes = Buffer.from(stableJson(plan.row));
370
+ writeCreateOnly(continuationBeforeimagePath(plan, digestBytes(rowBytes)), rowBytes, digestBytes(rowBytes));
371
+ writeCommittedContinuationCheckpoint(continuationIntentPath(plan), next);
372
+ intent = next;
373
+ },
374
+ commit: () => {
375
+ if (!intent)
376
+ throw new Error('Committed continuation lacks its durable intent.');
377
+ const { registry, row } = readCurrent();
378
+ if (stableJson(row) !== stableJson(current))
379
+ throw new Error('Committed continuation canonical row changed before commit.');
380
+ assertContinuationPreserved(intent.plan, true);
381
+ const replacementOwner = ownerContent(intent.plan.replacement);
382
+ writeCommittedContinuationCheckpoint(ownerPath(root, intent.plan.replacement), {
383
+ ...replacementOwner,
384
+ ownerId: digestValue(replacementOwner),
385
+ });
386
+ registry.worktrees.splice(registry.worktrees.indexOf(row), 1, intent.plan.replacement);
387
+ saveRegistryStrict(root, registry);
388
+ current = intent.plan.replacement;
389
+ },
390
+ complete: () => {
391
+ if (!intent || stableJson(readCurrent().row) !== stableJson(intent.plan.replacement))
392
+ throw new Error('Committed continuation replacement row is incomplete.');
393
+ assertContinuationPreserved(intent.plan, true);
394
+ assertOwnerWitness(intent.plan.replacement, root);
395
+ const content = {
396
+ schemaVersion: 'ai-delivery.committed-continuation-completion@1',
397
+ intentId: intent.intentId,
398
+ planId: intent.plan.planId,
399
+ row: intent.plan.replacement,
400
+ head: intent.plan.head,
401
+ completed: true,
402
+ };
403
+ writeCommittedContinuationCheckpoint(continuationCompletionPath(intent.plan), {
404
+ ...content,
405
+ completionId: digestValue(content),
406
+ });
407
+ },
408
+ });
409
+ },
410
+ });
411
+ }
115
412
  /** @internal The explicit transition owner holds the existing registry lock through its exact planned mutation. */
116
413
  export async function withWorktreeTransitionRegistry(plan, operation) {
117
414
  const root = plan.repoRoot;
415
+ if (pendingCommittedContinuation(plan.row.issueNumber, root))
416
+ throw new Error('Committed continuation is pending or incomplete; resume supported develop.');
118
417
  ensureRegistryDirectory(root);
119
418
  return withLock(REGISTRY_PATH, {
120
419
  projectRoot: root,