@aviaratech/ai-delivery 0.3.5 → 0.3.7

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 (38) hide show
  1. package/README.md +65 -1
  2. package/dist/agent.d.ts +1 -1
  3. package/dist/agent.js +1 -1
  4. package/dist/agent.js.map +1 -1
  5. package/dist/cli.js +35 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/dispatch.js +21 -1
  8. package/dist/dispatch.js.map +1 -1
  9. package/dist/github/commandIdentityPolicy.js +1 -0
  10. package/dist/github/commandIdentityPolicy.js.map +1 -1
  11. package/dist/issue.d.ts +1 -0
  12. package/dist/issue.js +22 -10
  13. package/dist/issue.js.map +1 -1
  14. package/dist/lifecycle.test.js +470 -4
  15. package/dist/lifecycle.test.js.map +1 -1
  16. package/dist/mcp/index.js +1 -1
  17. package/dist/mcp/tools.d.ts +31 -0
  18. package/dist/mcp/tools.js +27 -0
  19. package/dist/mcp/tools.js.map +1 -1
  20. package/dist/pr.d.ts +1 -1
  21. package/dist/services/worktreeRegistry.d.ts +12 -0
  22. package/dist/services/worktreeRegistry.js +146 -4
  23. package/dist/services/worktreeRegistry.js.map +1 -1
  24. package/dist/verification.d.ts +34 -0
  25. package/dist/verification.js +488 -64
  26. package/dist/verification.js.map +1 -1
  27. package/dist/worktree.d.ts +3 -0
  28. package/dist/worktree.js +38 -8
  29. package/dist/worktree.js.map +1 -1
  30. package/dist/worktreeTransition.d.ts +173 -0
  31. package/dist/worktreeTransition.js +1012 -0
  32. package/dist/worktreeTransition.js.map +1 -0
  33. package/dist/worktreeTransition.test.d.ts +1 -0
  34. package/dist/worktreeTransition.test.js +1179 -0
  35. package/dist/worktreeTransition.test.js.map +1 -0
  36. package/package.json +1 -1
  37. package/plugins/ai-delivery/.claude-plugin/plugin.json +1 -1
  38. package/plugins/ai-delivery/skills/worktree-lifecycle/SKILL.md +6 -0
@@ -0,0 +1,1179 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, unlinkSync, utimesSync, writeFileSync, } from 'node:fs';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { test, vi } from 'vitest';
9
+ import { digestBytes, digestValue, stableJson } from './delivery/common.js';
10
+ import { classifyRepositoryExactRange, createRepositoryStageInput, createRepositoryStageReceipt, createRepositoryStageAggregate, writeRepositoryStageCheckpoint, } from './delivery/index.js';
11
+ import { buildRuntimeAdmission } from './services/deliveryAdmission.js';
12
+ import { gitCommonDir } from './git.js';
13
+ import { addWorktreeEntry, assertNoForeignIssueWorktree, getIssueWorktreeStrict, listWorktreesStrict, withWorktreeTransitionRegistry, } from './services/worktreeRegistry.js';
14
+ import { prepareIssueWorktree } from './worktree.js';
15
+ import { withWorktreeTransitionWriterLease } from './verification.js';
16
+ import { applyWorktreeTransition, assertWorktreeTransitionNativeAuthority, inspectWorktreeTransition, inventoryTransitionEvidence, preserveTransitionEvidence, worktreeAcceptanceBody, worktreeRelinquishmentBody, } from './worktreeTransition.js';
17
+ import { assertWorktreeTransitionWriterQuiescent, loadVerifiedRun, retainedWorktreeTransitionProducerDigest, } from './verification.js';
18
+ const nativeAuthority = vi.hoisted(() => ({
19
+ reviewer: 'configured-reviewer[bot]',
20
+ comments: new Map(),
21
+ revoked: [],
22
+ }));
23
+ vi.mock('./github/client.js', async (original) => ({
24
+ ...(await original()),
25
+ createDeliveryGitHubClients: async () => ({
26
+ authSource: 'app',
27
+ role: 'reviewer',
28
+ appActorLogin: async () => nativeAuthority.reviewer,
29
+ rest: {
30
+ issues: {
31
+ getComment: async ({ comment_id }) => ({
32
+ data: nativeAuthority.comments.get(comment_id),
33
+ }),
34
+ },
35
+ },
36
+ }),
37
+ }));
38
+ function authorityFixture(root) {
39
+ nativeAuthority.comments.clear();
40
+ nativeAuthority.revoked = [];
41
+ nativeAuthority.reviewer = 'configured-reviewer[bot]';
42
+ // Authority-unit tests deliberately isolate native identity/body validation from installed producer and Git proof.
43
+ const digest = digestValue('synthetic immutable bytes');
44
+ const runtimeContent = {
45
+ schemaVersion: 'ai-delivery.runtime-admission@2',
46
+ capability: { cli: 2, mcp: 2 },
47
+ repository: 'example/widget',
48
+ cliPath: join(root, 'synthetic-runtime/dist/cli.js'),
49
+ cliSha256: digest,
50
+ configDigest: digest,
51
+ mcpLauncherPath: join(root, 'synthetic-runtime/plugins/ai-delivery/dist/mcp-launcher.js'),
52
+ mcpLauncherSha256: digest,
53
+ pluginManifestPath: join(root, 'synthetic-runtime/plugins/ai-delivery/.claude-plugin/plugin.json'),
54
+ pluginManifestSha256: digest,
55
+ packageDistSha256: digest,
56
+ packageManifestSha256: digest,
57
+ packageVersion: '0.3.5',
58
+ sourceArchiveSha256: digest,
59
+ sourceCommit: 'a'.repeat(40),
60
+ };
61
+ const admission = { ...runtimeContent, admissionId: digestValue(runtimeContent) };
62
+ const content = {
63
+ schemaVersion: 'ai-delivery.worktree-transition-plan@1',
64
+ repository: 'example/widget',
65
+ repoRoot: root,
66
+ row: entry(root),
67
+ purpose: 'active-resume',
68
+ disposition: 'retain',
69
+ terminalPrNumber: null,
70
+ head: { sha: 'a'.repeat(40), tree: 'b'.repeat(40) },
71
+ lineage: [],
72
+ remoteRefs: null,
73
+ retainedHoldCommentIds: [],
74
+ holdEvidence: [],
75
+ inventory: [],
76
+ inventoryId: digestValue([]),
77
+ historicalProducer: 'UNKNOWN',
78
+ closure: {
79
+ family: 'public-ai-delivery-0.3.5-posix@1',
80
+ admissionPath: join(root, 'synthetic-admission.json'),
81
+ archivePath: join(root, 'synthetic-archive.tar.gz'),
82
+ admission,
83
+ admissionBytesDigest: digest,
84
+ producerDigest: digest,
85
+ runManifestIds: [digest],
86
+ },
87
+ currentRuntime: admission,
88
+ policyDigest: digest,
89
+ operator: { actorLogin: 'configured-host', credentialIdentity: 'user:37' },
90
+ reviewerActor: nativeAuthority.reviewer,
91
+ };
92
+ const plan = { ...content, planId: digestValue(content) };
93
+ const context = {
94
+ root,
95
+ repo: { owner: 'example', repo: 'widget' },
96
+ config: { roles: { reviewer: { identity: 'configured-reviewer' } } },
97
+ clients: {
98
+ authSource: 'personal',
99
+ role: 'author',
100
+ authenticatedAuthor: async () => plan.operator,
101
+ rest: {
102
+ issues: {
103
+ getComment: async ({ comment_id }) => ({
104
+ data: nativeAuthority.comments.get(comment_id),
105
+ }),
106
+ listComments: async () => ({ data: nativeAuthority.revoked }),
107
+ },
108
+ },
109
+ },
110
+ };
111
+ const issue_url = 'https://api.github.com/repos/example/widget/issues/17';
112
+ nativeAuthority.comments.set(101, {
113
+ id: 101,
114
+ issue_url,
115
+ user: { id: 37, login: 'configured-host', type: 'User' },
116
+ body: worktreeRelinquishmentBody(plan),
117
+ });
118
+ nativeAuthority.comments.set(102, {
119
+ id: 102,
120
+ issue_url,
121
+ user: { id: 38, login: nativeAuthority.reviewer, type: 'Bot' },
122
+ body: worktreeAcceptanceBody(plan, 101),
123
+ });
124
+ return { plan, context, ids: { relinquishmentCommentId: 101, acceptanceCommentId: 102 } };
125
+ }
126
+ function approvedFixturePlan(f, plan) {
127
+ const planPath = join(f.root, 'approved-plan.json');
128
+ writeFileSync(planPath, stableJson(plan));
129
+ nativeAuthority.comments.get(101).body = worktreeRelinquishmentBody(plan);
130
+ nativeAuthority.comments.get(102).body = worktreeAcceptanceBody(plan, 101);
131
+ return {
132
+ authority: 'worktree:transition',
133
+ planPath,
134
+ expectedPlanId: plan.planId,
135
+ relinquishmentCommentId: 101,
136
+ acceptanceCommentId: 102,
137
+ runtimeEntryPath: f.runtimeEntryPath,
138
+ };
139
+ }
140
+ function interruptedApply(f, plan, event) {
141
+ const input = approvedFixturePlan(f, plan);
142
+ const commonUrl = new URL('./delivery/common.js', import.meta.url).href;
143
+ const clientUrl = new URL('./github/client.js', import.meta.url).href;
144
+ const inputPath = join(f.root, 'child-transition-input.json');
145
+ writeFileSync(inputPath, stableJson({
146
+ input,
147
+ event,
148
+ plan,
149
+ context: { root: f.root, repo: f.context.repo, config: f.context.config, configuration: f.context.configuration },
150
+ comments: [...nativeAuthority.comments],
151
+ nativePr: f.nativePr,
152
+ }));
153
+ // Only GitHub HTTP/auth is substituted. Files, admission, producer hashing, Git and process identities stay real.
154
+ const script = `
155
+ import { registerHooks } from 'node:module';
156
+ import { readFileSync } from 'node:fs';
157
+ const f=JSON.parse(readFileSync(process.argv[1],'utf8'));
158
+ const comments=new Map(f.comments);
159
+ const issues={getComment:async({comment_id})=>({data:comments.get(comment_id)}),listComments:async()=>({data:[]}),get:async()=>({data:{state:f.plan.purpose==='merged-cleanup'?'closed':'open'}})};
160
+ globalThis.__transitionReviewer={authSource:'app',role:'reviewer',appActorLogin:async()=>f.plan.reviewerActor,rest:{issues}};
161
+ registerHooks({load(url,context,next){
162
+ if(url===${JSON.stringify(clientUrl)})return {format:'module',shortCircuit:true,source:
163
+ 'export * from '+JSON.stringify(url+'?original')+'; export async function createDeliveryGitHubClients(){return globalThis.__transitionReviewer;}'};
164
+ if(url===${JSON.stringify(commonUrl)})return {format:'module',shortCircuit:true,source:
165
+ 'export * from '+JSON.stringify(url+'?original')+'; import {writeCreateOnly as original} from '+JSON.stringify(url+'?original')+'; export function writeCreateOnly(path,bytes,digest){const value=original(path,bytes,digest);if(globalThis.__transitionCrash(path))process.exit(73);return value;}'};
166
+ return next(url,context);
167
+ }});
168
+ globalThis.__transitionCrash=(path)=> f.event==='intent' ? path.endsWith('.transition.json') : f.event==='completion' ? path.endsWith('/completion.json') : path.endsWith('.'+f.event+'.json');
169
+ const {applyWorktreeTransition}=await import(${JSON.stringify(new URL('./worktreeTransition.js', import.meta.url).href)});
170
+ const context={...f.context,clients:{authSource:'personal',role:'author',authenticatedAuthor:async()=>f.plan.operator,
171
+ graphql:async()=>({repository:{pullRequest:{closingIssuesReferences:{nodes:[{number:17,repository:{nameWithOwner:'example/widget'}}],pageInfo:{hasNextPage:false}}}}}),
172
+ rest:{issues,pulls:{get:async()=>({data:f.nativePr})},repos:{getBranch:async({branch})=>({data:{name:branch,commit:{sha:f.plan.remoteRefs?.baseSha??f.plan.head.sha}}})}}}};
173
+ await applyWorktreeTransition(context,f.input);
174
+ process.exit(9);
175
+ `;
176
+ const child = spawnSync(process.execPath, ['--input-type=module', '-e', script, inputPath], {
177
+ encoding: 'utf8',
178
+ maxBuffer: 1024 * 1024,
179
+ });
180
+ assert.equal(child.status, 73, child.stderr);
181
+ // The child is confirmed exited. Advance only its synthetic fixture lock mtimes to exercise stale-lock recovery promptly.
182
+ const common = gitCommonDir(f.root);
183
+ for (const lock of [
184
+ join(f.root, '.issue-cli/worktrees.json.lock'),
185
+ join(common, 'ai-delivery/writers@1', `${digestValue(f.row.path).slice(7)}.json.lock`),
186
+ ])
187
+ if (existsSync(lock))
188
+ utimesSync(lock, new Date(0), new Date(0));
189
+ return input;
190
+ }
191
+ test('real process interruption resumes original capture, claim, witness, row and release without losing originals', { timeout: 0 }, async () => {
192
+ for (const event of ['intent', 'authority', 'claim', 'claimed', 'owner', 'row', 'release', 'released']) {
193
+ const f = await transitionFixture();
194
+ try {
195
+ const original = previousWriter(f.root);
196
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
197
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
198
+ const plan = inspected.plan;
199
+ const input = interruptedApply(f, plan, event);
200
+ assert.throws(() => getIssueWorktreeStrict(17, f.root), /transition.*incomplete/iu);
201
+ const completed = await applyWorktreeTransition(f.context, input);
202
+ assert.equal(completed.result, 'active-resumed', event);
203
+ assert.equal(getIssueWorktreeStrict(17, f.root).identity, f.row.identity);
204
+ const evidence = join(dirname(completed.receiptPath), 'bytes', `${digestBytes(original.bytes).slice(7)}.bin`);
205
+ assert.deepEqual(readFileSync(evidence), original.bytes, event);
206
+ assert.equal(existsSync(original.path), false, event);
207
+ }
208
+ finally {
209
+ rmSync(f.root, { force: true, recursive: true });
210
+ }
211
+ }
212
+ });
213
+ test('terminal interruption resumes removal intent, physical absence and row removal without recreating source', { timeout: 0 }, async () => {
214
+ for (const event of ['removal-intent', 'removed', 'released', 'completion']) {
215
+ const f = await transitionFixture({ purpose: 'merged-cleanup', disposition: 'remove' });
216
+ try {
217
+ const local = join(f.row.path, '.issue-cli', 'receipts');
218
+ mkdirSync(local, { recursive: true });
219
+ const original = Buffer.from('synthetic original terminal accounting and failed diagnostic\n');
220
+ writeFileSync(join(local, 'original.json'), original);
221
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
222
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
223
+ const plan = inspected.plan;
224
+ const input = interruptedApply(f, plan, event);
225
+ const applied = await applyWorktreeTransition(f.context, input);
226
+ assert.equal(applied.result, 'removed', event);
227
+ assert.equal(existsSync(f.row.path), false, event);
228
+ assert.deepEqual(listWorktreesStrict(f.root), [], event);
229
+ assert.deepEqual(readFileSync(join(dirname(applied.receiptPath), 'bytes', `${digestBytes(original).slice(7)}.bin`)), original, event);
230
+ assert.throws(() => assertNoForeignIssueWorktree(17, f.root), /terminal/iu);
231
+ }
232
+ finally {
233
+ rmSync(f.root, { force: true, recursive: true });
234
+ }
235
+ }
236
+ });
237
+ test('an earlier released attempt cannot authorize absence after a later acknowledged claim', { timeout: 0 }, async () => {
238
+ const f = await transitionFixture();
239
+ try {
240
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
241
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
242
+ const plan = inspected.plan;
243
+ interruptedApply(f, plan, 'released');
244
+ const input = interruptedApply(f, plan, 'claimed');
245
+ const writerPath = join(gitCommonDir(f.root), 'ai-delivery/writers@1', `${digestValue(f.row.path).slice(7)}.json`);
246
+ assert.equal(existsSync(writerPath), true);
247
+ const originalClaim = readFileSync(writerPath);
248
+ unlinkSync(writerPath);
249
+ await assert.rejects(applyWorktreeTransition(f.context, input), /latest exact.*lineage/iu);
250
+ // Restore only the exact recorded claim, then resume successfully; no fabricated witness is accepted.
251
+ writeFileSync(writerPath, originalClaim, { mode: 0o600 });
252
+ assert.equal((await applyWorktreeTransition(f.context, input)).result, 'active-resumed');
253
+ }
254
+ finally {
255
+ rmSync(f.root, { force: true, recursive: true });
256
+ }
257
+ });
258
+ test('transition authority requires exact native personal and configured App identities and whole bodies', async () => {
259
+ const root = repository();
260
+ try {
261
+ for (const change of [
262
+ 'operator-id',
263
+ 'operator-body',
264
+ 'reviewer-actor',
265
+ 'acceptance-body',
266
+ 'subject',
267
+ 'comment-id',
268
+ ]) {
269
+ const { plan, context, ids } = authorityFixture(root);
270
+ const operator = nativeAuthority.comments.get(101);
271
+ const reviewer = nativeAuthority.comments.get(102);
272
+ if (change === 'operator-id')
273
+ operator.user = { id: 99, login: 'configured-host', type: 'User' };
274
+ if (change === 'operator-body')
275
+ operator.body = `${String(operator.body)}\n`;
276
+ if (change === 'reviewer-actor')
277
+ reviewer.user = { id: 39, login: 'caller-selected-reviewer[bot]', type: 'Bot' };
278
+ if (change === 'acceptance-body')
279
+ reviewer.body = worktreeAcceptanceBody(plan, 999);
280
+ if (change === 'subject')
281
+ reviewer.issue_url = 'https://api.github.com/repos/example/widget/issues/99';
282
+ if (change === 'comment-id')
283
+ reviewer.id = 999;
284
+ await assert.rejects(assertWorktreeTransitionNativeAuthority(context, plan, ids), /Native .* (?:missing|changed)/u);
285
+ }
286
+ const { plan, context, ids } = authorityFixture(root);
287
+ await assertWorktreeTransitionNativeAuthority(context, plan, ids);
288
+ }
289
+ finally {
290
+ rmSync(root, { force: true, recursive: true });
291
+ }
292
+ });
293
+ test('only authenticated exact relinquishment revocations invalidate native acceptance', async () => {
294
+ const root = repository();
295
+ try {
296
+ const { plan, context, ids } = authorityFixture(root);
297
+ const body = JSON.stringify({
298
+ schemaVersion: 'ai-delivery.worktree-relinquishment-revocation@1',
299
+ planId: plan.planId,
300
+ relinquishmentCommentId: 101,
301
+ });
302
+ const stableBody = Object.fromEntries(Object.entries(JSON.parse(body)).sort(([a], [b]) => a.localeCompare(b)));
303
+ nativeAuthority.revoked = [{ body: JSON.stringify(stableBody), user: { id: 99, login: 'forged-host' } }];
304
+ await assertWorktreeTransitionNativeAuthority(context, plan, ids);
305
+ nativeAuthority.revoked[0].user = { id: 37, login: 'configured-host' };
306
+ await assert.rejects(assertWorktreeTransitionNativeAuthority(context, plan, ids), /revoked/u);
307
+ }
308
+ finally {
309
+ rmSync(root, { force: true, recursive: true });
310
+ }
311
+ });
312
+ function repository() {
313
+ const root = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-transition-')));
314
+ const git = (...args) => execFileSync('git', args, { cwd: root, stdio: 'pipe' });
315
+ git('init', '-b', 'main');
316
+ git('config', 'user.name', 'Synthetic Operator');
317
+ git('config', 'user.email', 'operator@example.test');
318
+ writeFileSync(join(root, 'source.txt'), 'synthetic source\n');
319
+ git('add', 'source.txt');
320
+ git('commit', '-m', 'synthetic source');
321
+ writeFileSync(join(root, '.git', 'info', 'exclude'), '.issue-cli/\n');
322
+ git('worktree', 'add', '-b', 'issue/17', join(root, '.worktrees', 'issue-17'));
323
+ return root;
324
+ }
325
+ async function transitionFixture(options = {}) {
326
+ const root = repository();
327
+ const row = entry(root);
328
+ const { context } = authorityFixture(root);
329
+ const configuration = {
330
+ config: { ...context.config, repository: 'example/widget' },
331
+ configDigest: digestValue('synthetic current configuration'),
332
+ policyModulePath: join(root, 'source.txt'),
333
+ remote: 'origin',
334
+ };
335
+ context.config = configuration.config;
336
+ context.configuration = configuration;
337
+ context.clients.rest.issues.get = (async () => ({
338
+ data: { state: 'open' },
339
+ }));
340
+ mkdirSync(join(root, '.issue-cli'), { recursive: true });
341
+ writeFileSync(join(root, '.issue-cli', 'worktrees.json'), JSON.stringify({ worktrees: [row] }), { mode: 0o600 });
342
+ const oldDirectory = join(root, '.retained-producer');
343
+ mkdirSync(oldDirectory);
344
+ // Official npm 0.3.5 distribution, fixed SHA-256; only unpack/read its bytes. Never execute retained code.
345
+ const archivePath = fileURLToPath(new URL('../src/fixtures/public-ai-delivery-0.3.5.tar.gz', import.meta.url));
346
+ execFileSync('tar', ['-xzf', archivePath, '-C', oldDirectory]);
347
+ const packageRoot = join(oldDirectory, 'package');
348
+ const thisPackage = dirname(dirname(fileURLToPath(import.meta.url)));
349
+ symlinkSync(join(thisPackage, 'node_modules'), join(root, 'node_modules'));
350
+ const admission = buildRuntimeAdmission({
351
+ cliPath: join(packageRoot, 'dist', 'cli.js'),
352
+ mcpLauncherPath: join(packageRoot, 'plugins/ai-delivery/dist/mcp-launcher.js'),
353
+ pluginManifestPath: join(packageRoot, 'plugins/ai-delivery/.claude-plugin/plugin.json'),
354
+ packageVersion: '0.3.5',
355
+ sourceArchiveSha256: digestBytes(readFileSync(archivePath)),
356
+ sourceCommit: 'a'.repeat(40),
357
+ configuration,
358
+ });
359
+ const admissionPath = join(root, '.retained-admission.json');
360
+ writeFileSync(admissionPath, JSON.stringify(admission), { mode: 0o600 });
361
+ const runtimeEntryPath = join(thisPackage, 'dist', 'cli.js');
362
+ const currentRuntime = buildRuntimeAdmission({
363
+ cliPath: runtimeEntryPath,
364
+ mcpLauncherPath: join(thisPackage, 'plugins/ai-delivery/dist/mcp-launcher.js'),
365
+ pluginManifestPath: join(thisPackage, 'plugins/ai-delivery/.claude-plugin/plugin.json'),
366
+ packageVersion: JSON.parse(readFileSync(join(thisPackage, 'package.json'), 'utf8'))
367
+ .version,
368
+ sourceArchiveSha256: digestValue('synthetic reviewed current archive'),
369
+ sourceCommit: 'b'.repeat(40),
370
+ configuration,
371
+ });
372
+ const common = gitCommonDir(root);
373
+ mkdirSync(join(common, 'ai-delivery'), { recursive: true, mode: 0o700 });
374
+ writeFileSync(join(common, 'ai-delivery', 'runtime-admission.json'), JSON.stringify(currentRuntime), { mode: 0o600 });
375
+ const sha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: row.path, encoding: 'utf8' }).trim();
376
+ const tree = execFileSync('git', ['rev-parse', 'HEAD^{tree}'], { cwd: row.path, encoding: 'utf8' }).trim();
377
+ const sourceDigest = digestBytes(readFileSync(join(row.path, 'source.txt')));
378
+ const policy = {
379
+ schemaVersion: 'RepositoryDeliveryPolicy@1',
380
+ classifyExactRange: () => {
381
+ const content = {
382
+ schemaVersion: 'ai-delivery.policy-evidence@1',
383
+ repository: 'example/widget',
384
+ base: { sha, tree },
385
+ head: { sha, tree },
386
+ configDigest: configuration.configDigest,
387
+ policyDigest: sourceDigest,
388
+ producer: 'synthetic-history-policy',
389
+ opaquePayload: 'synthetic history',
390
+ artifacts: [{ digest: sourceDigest, path: 'source.txt', producer: 'synthetic-history-policy' }],
391
+ };
392
+ return {
393
+ policyDigest: sourceDigest,
394
+ policyEvidence: { ...content, evidenceId: digestValue(content) },
395
+ risk: 'standard',
396
+ requiredStages: [
397
+ {
398
+ id: 'source',
399
+ resourceClass: 'source_only',
400
+ commands: [],
401
+ attestationKey: 'source.txt',
402
+ dependsOn: [],
403
+ semanticInputKeys: ['source'],
404
+ },
405
+ ],
406
+ };
407
+ },
408
+ };
409
+ const classification = classifyRepositoryExactRange({
410
+ repoRoot: row.path,
411
+ repository: 'example/widget',
412
+ base: { sha, tree },
413
+ head: { sha, tree },
414
+ changedPaths: [],
415
+ configDigest: configuration.configDigest,
416
+ policySourcePath: 'source.txt',
417
+ policy,
418
+ });
419
+ const producerDigest = retainedWorktreeTransitionProducerDigest(join(packageRoot, 'dist'));
420
+ const stageInput = createRepositoryStageInput({
421
+ classification,
422
+ stageId: 'source',
423
+ semanticInputs: [{ key: 'source', digest: sourceDigest }],
424
+ upstream: [],
425
+ environmentDigest: digestValue({
426
+ arch: process.arch,
427
+ node: process.version,
428
+ path: process.env.PATH ?? '',
429
+ platform: process.platform,
430
+ worktreeDigest: digestValue(row.path),
431
+ producerDigest,
432
+ }),
433
+ });
434
+ const receipt = createRepositoryStageReceipt({
435
+ classification,
436
+ stageInput,
437
+ commands: [],
438
+ artifacts: [{ digest: sourceDigest, path: 'source.txt' }],
439
+ startedAt: '2026-01-01T00:00:00.000Z',
440
+ completedAt: '2026-01-01T00:00:00.000Z',
441
+ });
442
+ writeRepositoryStageCheckpoint({ gitCommonDir: common, repoRoot: row.path, receipt });
443
+ const runContent = {
444
+ schemaVersion: 'ai-delivery.run@3',
445
+ completedAt: '2026-01-01T00:00:00.000Z',
446
+ classification,
447
+ aggregate: createRepositoryStageAggregate({ classification, receipts: [receipt] }),
448
+ stageReceipts: [receipt],
449
+ writer: { worktreeDigest: digestValue(row.path), producerDigest },
450
+ };
451
+ const runs = join(common, 'ai-delivery', 'runs@2', digestValue(row.path).slice(7));
452
+ mkdirSync(runs, { recursive: true, mode: 0o700 });
453
+ writeFileSync(join(runs, `${sha}.json`), JSON.stringify({ ...runContent, manifestId: digestValue(runContent) }), {
454
+ mode: 0o600,
455
+ });
456
+ const nativePr = {
457
+ number: 23,
458
+ head: { sha, ref: row.branch, repo: { full_name: 'example/widget' } },
459
+ base: { sha, ref: 'main', repo: { full_name: 'example/widget' } },
460
+ merge_commit_sha: sha,
461
+ merged: true,
462
+ user: { login: 'synthetic-original-author' },
463
+ };
464
+ if (options.purpose === 'merged-cleanup') {
465
+ row.status = 'pr-published';
466
+ row.prNumber = 23;
467
+ writeFileSync(join(root, '.issue-cli/worktrees.json'), JSON.stringify({ worktrees: [row] }), { mode: 0o600 });
468
+ execFileSync('git', ['remote', 'add', 'origin', 'https://github.com/example/widget.git'], { cwd: root });
469
+ execFileSync('git', ['update-ref', 'refs/remotes/origin/main', sha], { cwd: root });
470
+ context.clients.rest.issues.get = (async () => ({
471
+ data: { state: 'closed' },
472
+ }));
473
+ context.clients.rest.pulls = {
474
+ get: async () => ({ data: nativePr }),
475
+ };
476
+ context.clients.rest.repos = {
477
+ getBranch: async ({ branch }) => ({ data: { name: branch, commit: { sha } } }),
478
+ };
479
+ context.clients.graphql = (async () => ({
480
+ repository: {
481
+ pullRequest: {
482
+ closingIssuesReferences: {
483
+ nodes: [{ number: 17, repository: { nameWithOwner: 'example/widget' } }],
484
+ pageInfo: { hasNextPage: false },
485
+ },
486
+ },
487
+ },
488
+ }));
489
+ for (const id of [101, 102])
490
+ nativeAuthority.comments.get(id).issue_url = 'https://api.github.com/repos/example/widget/issues/23';
491
+ if (options.held)
492
+ nativeAuthority.comments.set(103, {
493
+ id: 103,
494
+ issue_url: 'https://api.github.com/repos/example/widget/issues/23',
495
+ body: 'Synthetic retained source hold; transition does not release it.',
496
+ user: { id: 37, login: 'configured-host', type: 'User' },
497
+ });
498
+ }
499
+ return {
500
+ root,
501
+ row,
502
+ context,
503
+ runtimeEntryPath,
504
+ admissionPath,
505
+ archivePath,
506
+ nativePr,
507
+ inspectInput: {
508
+ issueNumber: 17,
509
+ purpose: options.purpose ?? 'active-resume',
510
+ disposition: options.disposition ?? 'retain',
511
+ ...(options.held ? { retainedHoldCommentIds: [103] } : {}),
512
+ retainedAdmissionPath: admissionPath,
513
+ retainedArchivePath: archivePath,
514
+ runtimeEntryPath,
515
+ },
516
+ };
517
+ }
518
+ test('actual retained public bytes support one explicit independently accepted active resume', async () => {
519
+ const f = await transitionFixture();
520
+ try {
521
+ const inspection = await inspectWorktreeTransition(f.context, f.inspectInput);
522
+ assert.equal(inspection.ready, true, inspection.blockers.join('; '));
523
+ const plan = inspection.plan;
524
+ assert.ok(worktreeRelinquishmentBody(plan).length < 65_000, 'native authority body must fit the comment boundary while binding the full plan');
525
+ for (const path of [f.admissionPath, f.archivePath, plan.closure.admission.cliPath])
526
+ assert.ok(plan.inventory.some((file) => file.source === path), `retained operational producer original omitted: ${path}`);
527
+ const planPath = join(f.root, 'approved-plan.json');
528
+ writeFileSync(planPath, stableJson(plan));
529
+ nativeAuthority.comments.get(101).body = worktreeRelinquishmentBody(plan);
530
+ nativeAuthority.comments.get(102).body = worktreeAcceptanceBody(plan, 101);
531
+ const applied = await applyWorktreeTransition(f.context, {
532
+ authority: 'worktree:transition',
533
+ planPath,
534
+ expectedPlanId: plan.planId,
535
+ relinquishmentCommentId: 101,
536
+ acceptanceCommentId: 102,
537
+ runtimeEntryPath: f.runtimeEntryPath,
538
+ });
539
+ assert.equal(applied.result, 'active-resumed');
540
+ assert.equal(getIssueWorktreeStrict(17, f.root).identity, f.row.identity);
541
+ assert.throws(() => loadVerifiedRun(f.row.path, 17), /verification.*(?:missing|corrupt|incompatible)|producer|run/iu, 'preserved retained-producer history cannot satisfy current verification');
542
+ assert.equal(digestBytes(readFileSync(f.admissionPath)), plan.closure.admissionBytesDigest);
543
+ const replayed = await applyWorktreeTransition(f.context, {
544
+ authority: 'worktree:transition',
545
+ planPath,
546
+ expectedPlanId: plan.planId,
547
+ relinquishmentCommentId: 101,
548
+ acceptanceCommentId: 102,
549
+ runtimeEntryPath: f.runtimeEntryPath,
550
+ });
551
+ assert.equal(replayed.replayed, true);
552
+ }
553
+ finally {
554
+ rmSync(f.root, { force: true, recursive: true });
555
+ }
556
+ });
557
+ test('completed replay checks original attempt bytes, actual writer absence, row and owner witness', { timeout: 0 }, async () => {
558
+ const f = await transitionFixture();
559
+ try {
560
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
561
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
562
+ const plan = inspected.plan;
563
+ const input = approvedFixturePlan(f, plan);
564
+ const completed = await applyWorktreeTransition(f.context, input);
565
+ const directory = dirname(completed.receiptPath);
566
+ const attempt = readdirSync(join(directory, 'attempts')).find((name) => name.endsWith('.authority.json'));
567
+ const authority = JSON.parse(readFileSync(join(directory, 'attempts', attempt), 'utf8'));
568
+ const bytesPath = join(directory, 'bytes', `${authority.data.bytesDigest.slice(7)}.bin`);
569
+ const bytes = readFileSync(bytesPath);
570
+ writeFileSync(bytesPath, 'corrupt preserved native authority', { mode: 0o600 });
571
+ await assert.rejects(applyWorktreeTransition(f.context, input), /preserved.*corrupt/iu);
572
+ writeFileSync(bytesPath, bytes, { mode: 0o600 });
573
+ const writer = previousWriter(f.root);
574
+ await assert.rejects(applyWorktreeTransition(f.context, input), /writer absence postcondition/iu);
575
+ assert.deepEqual(readFileSync(writer.path), writer.bytes, 'replay must not signal or replace a writer');
576
+ unlinkSync(writer.path);
577
+ const registry = join(f.root, '.issue-cli/worktrees.json');
578
+ const registryBytes = readFileSync(registry);
579
+ writeFileSync(registry, JSON.stringify({ worktrees: [{ ...f.row, status: 'stale' }] }), { mode: 0o600 });
580
+ await assert.rejects(applyWorktreeTransition(f.context, input), /row|registry/iu);
581
+ writeFileSync(registry, registryBytes, { mode: 0o600 });
582
+ const owners = join(gitCommonDir(f.root), 'ai-delivery/worktree-owners');
583
+ const witness = readdirSync(owners).find((name) => name.endsWith('.json') && !name.includes('.transition.'));
584
+ const witnessPath = join(owners, witness);
585
+ const witnessBytes = readFileSync(witnessPath);
586
+ writeFileSync(witnessPath, '{}', { mode: 0o600 });
587
+ await assert.rejects(applyWorktreeTransition(f.context, input), /owner witness/iu);
588
+ writeFileSync(witnessPath, witnessBytes, { mode: 0o600 });
589
+ assert.equal((await applyWorktreeTransition(f.context, input)).replayed, true);
590
+ }
591
+ finally {
592
+ rmSync(f.root, { force: true, recursive: true });
593
+ }
594
+ });
595
+ test('inspection and accepted apply refuse dirty source, wrong repository and changed canonical row without writing intent', { timeout: 0 }, async () => {
596
+ const f = await transitionFixture();
597
+ try {
598
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
599
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
600
+ const input = approvedFixturePlan(f, inspected.plan);
601
+ writeFileSync(join(f.row.path, 'untracked.txt'), 'dirty source\n');
602
+ assert.equal((await inspectWorktreeTransition(f.context, f.inspectInput)).ready, false);
603
+ await assert.rejects(applyWorktreeTransition(f.context, input), /dirty|clean|uncommitted/iu);
604
+ unlinkSync(join(f.row.path, 'untracked.txt'));
605
+ const repository = f.context.config.repository;
606
+ f.context.config.repository = 'example/another';
607
+ assert.match((await inspectWorktreeTransition(f.context, f.inspectInput)).blockers.join('; '), /repository/iu);
608
+ await assert.rejects(applyWorktreeTransition(f.context, input), /repository/iu);
609
+ f.context.config.repository = repository;
610
+ writeFileSync(join(f.root, '.issue-cli/worktrees.json'), JSON.stringify({ worktrees: [{ ...f.row, identity: 'another-owner' }] }), { mode: 0o600 });
611
+ await assert.rejects(applyWorktreeTransition(f.context, input), /row|ownership|owner/iu);
612
+ assert.equal(existsSync(join(gitCommonDir(f.root), 'ai-delivery/worktree-owners/issue-17.transition.json')), false);
613
+ }
614
+ finally {
615
+ rmSync(f.root, { force: true, recursive: true });
616
+ }
617
+ });
618
+ test('inspection reports unsupported operative v2 and missing or corrupt required historical originals', { timeout: 0 }, async () => {
619
+ const f = await transitionFixture();
620
+ try {
621
+ const unsupported = join(gitCommonDir(f.root), 'issue-cli/verification-stages/v2');
622
+ mkdirSync(unsupported, { recursive: true });
623
+ assert.match((await inspectWorktreeTransition(f.context, f.inspectInput)).blockers.join('; '), /unsupported operative.*v2/iu);
624
+ rmSync(unsupported, { recursive: true });
625
+ const directory = join(f.root, '.issue-cli/issues/17/terminal');
626
+ mkdirSync(directory, { recursive: true });
627
+ const original = Buffer.from('original failed diagnostic\n');
628
+ writeFileSync(join(directory, 'manifest.json'), JSON.stringify({
629
+ schemaVersion: 'issue-cli.terminal-evidence@1',
630
+ files: [{ name: 'failed.txt', size: original.length, sha256: digestBytes(original).slice(7) }],
631
+ }));
632
+ assert.equal((await inspectWorktreeTransition(f.context, f.inspectInput)).ready, false, 'missing original');
633
+ writeFileSync(join(directory, 'failed.txt'), 'corrupt');
634
+ assert.match((await inspectWorktreeTransition(f.context, f.inspectInput)).blockers.join('; '), /original bytes/iu);
635
+ writeFileSync(join(directory, 'failed.txt'), original);
636
+ assert.equal((await inspectWorktreeTransition(f.context, f.inspectInput)).ready, true);
637
+ }
638
+ finally {
639
+ rmSync(f.root, { force: true, recursive: true });
640
+ }
641
+ });
642
+ test('terminal inspection and accepted apply reject wrong native repository and PR lineage drift', { timeout: 0 }, async () => {
643
+ const f = await transitionFixture({ purpose: 'merged-cleanup' });
644
+ try {
645
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
646
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
647
+ const input = approvedFixturePlan(f, inspected.plan);
648
+ f.nativePr.head.repo.full_name = 'example/another';
649
+ assert.match((await inspectWorktreeTransition(f.context, f.inspectInput)).blockers.join('; '), /repository.*closing issue/iu);
650
+ await assert.rejects(applyWorktreeTransition(f.context, input), /repository.*closing issue/iu);
651
+ f.nativePr.head.repo.full_name = 'example/widget';
652
+ f.nativePr.user.login = 'changed-original-author';
653
+ await assert.rejects(applyWorktreeTransition(f.context, input), /native PR lineage.*drifted/iu);
654
+ }
655
+ finally {
656
+ rmSync(f.root, { force: true, recursive: true });
657
+ }
658
+ });
659
+ test('closure check refuses live owners, reused root PIDs and live process groups without signaling', () => {
660
+ const root = repository();
661
+ try {
662
+ const original = previousWriter(root);
663
+ const previous = JSON.parse(original.bytes.toString('utf8'));
664
+ const { stateId: _id, ...content } = previous;
665
+ const identity = execFileSync('/bin/ps', ['-p', String(process.pid), '-o', 'lstart='], {
666
+ encoding: 'utf8',
667
+ env: { ...process.env, LC_ALL: 'C' },
668
+ }).trim();
669
+ for (const reason of ['owner', 'reused-pid', 'group']) {
670
+ const state = {
671
+ ...content,
672
+ owner: reason === 'owner' ? { pid: process.pid, identity } : content.owner,
673
+ command: reason === 'owner'
674
+ ? { phase: 'idle' }
675
+ : {
676
+ phase: 'running',
677
+ root: { pid: reason === 'reused-pid' ? process.pid : 2_000_000_000, identity: 'unrelated old birth' },
678
+ tracked: reason === 'group'
679
+ ? [
680
+ {
681
+ pid: 2_000_000_001,
682
+ identity: 'old birth',
683
+ pgid: Number(execFileSync('/bin/ps', ['-p', String(process.pid), '-o', 'pgid='], {
684
+ encoding: 'utf8',
685
+ }).trim()),
686
+ },
687
+ ]
688
+ : [],
689
+ },
690
+ };
691
+ assert.throws(() => assertWorktreeTransitionWriterQuiescent(entry(root).path, Buffer.from(stableJson({ ...state, stateId: digestValue(state) }))), /live verification|process-group absence/iu);
692
+ process.kill(process.pid, 0);
693
+ }
694
+ assert.deepEqual(readFileSync(original.path), original.bytes);
695
+ }
696
+ finally {
697
+ rmSync(root, { force: true, recursive: true });
698
+ }
699
+ });
700
+ test('supported closure requires a run for the exact retained head and tree', async () => {
701
+ const f = await transitionFixture();
702
+ try {
703
+ writeFileSync(join(f.row.path, 'new-source.txt'), 'a newer synthetic retained source\n');
704
+ execFileSync('git', ['add', 'new-source.txt'], { cwd: f.row.path });
705
+ execFileSync('git', ['commit', '-m', 'new synthetic head'], { cwd: f.row.path });
706
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
707
+ assert.equal(inspected.ready, false, 'old producer-bound run cannot close a newer unverified retained source');
708
+ assert.match(inspected.blockers.join('; '), /exact.*(?:head|source)|closure/iu);
709
+ }
710
+ finally {
711
+ rmSync(f.root, { force: true, recursive: true });
712
+ }
713
+ });
714
+ test('apply revalidates the exact operational run inventory despite native acceptance of local JSON', async () => {
715
+ const f = await transitionFixture();
716
+ try {
717
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
718
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
719
+ const { planId: _originalId, ...content } = inspected.plan;
720
+ content.closure = { ...content.closure, runManifestIds: [digestValue('unrelated run')] };
721
+ const plan = { ...content, planId: digestValue(content) };
722
+ const planPath = join(f.root, 'changed-plan.json');
723
+ writeFileSync(planPath, stableJson(plan));
724
+ nativeAuthority.comments.get(101).body = worktreeRelinquishmentBody(plan);
725
+ nativeAuthority.comments.get(102).body = worktreeAcceptanceBody(plan, 101);
726
+ await assert.rejects(applyWorktreeTransition(f.context, {
727
+ authority: 'worktree:transition',
728
+ planPath,
729
+ expectedPlanId: plan.planId,
730
+ relinquishmentCommentId: 101,
731
+ acceptanceCommentId: 102,
732
+ runtimeEntryPath: f.runtimeEntryPath,
733
+ }), /run.*inventory|operational.*closure/iu);
734
+ }
735
+ finally {
736
+ rmSync(f.root, { force: true, recursive: true });
737
+ }
738
+ });
739
+ test('terminal transition retains source, holds and original PR identity and permanently rejects ordinary resume', async () => {
740
+ const f = await transitionFixture({ purpose: 'merged-cleanup', held: true });
741
+ try {
742
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
743
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
744
+ const plan = inspected.plan;
745
+ const applied = await applyWorktreeTransition(f.context, approvedFixturePlan(f, plan));
746
+ assert.equal(applied.result, 'retained');
747
+ assert.equal(existsSync(f.row.path), true);
748
+ assert.deepEqual(listWorktreesStrict(f.root), [{ ...f.row, status: 'merged' }]);
749
+ assert.deepEqual(plan.retainedHoldCommentIds, [103]);
750
+ assert.match(String(nativeAuthority.comments.get(103).body), /does not release/);
751
+ await assert.rejects(prepareIssueWorktree({ repoRoot: f.root, issueNumber: 17, identity: 'configured-host' }), /terminal/iu);
752
+ const hold = plan.holdEvidence[0];
753
+ assert.equal(digestBytes(readFileSync(join(dirname(applied.receiptPath), 'bytes', `${hold.digest.slice(7)}.bin`))), hold.digest);
754
+ }
755
+ finally {
756
+ rmSync(f.root, { force: true, recursive: true });
757
+ }
758
+ });
759
+ test('unheld terminal removal checks native remote refs and completed replay rejects recreated source', { timeout: 0 }, async () => {
760
+ const f = await transitionFixture({ purpose: 'merged-cleanup', disposition: 'remove' });
761
+ try {
762
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
763
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
764
+ const plan = inspected.plan;
765
+ const input = approvedFixturePlan(f, plan);
766
+ const applied = await applyWorktreeTransition(f.context, input);
767
+ assert.equal(applied.result, 'removed');
768
+ assert.equal(existsSync(f.row.path), false);
769
+ assert.deepEqual(listWorktreesStrict(f.root), []);
770
+ assert.equal((await applyWorktreeTransition(f.context, input)).replayed, true);
771
+ execFileSync('git', ['worktree', 'add', f.row.path, f.row.branch], { cwd: f.root });
772
+ await assert.rejects(applyWorktreeTransition(f.context, input), /recreated|absence postcondition/iu);
773
+ assert.throws(() => assertNoForeignIssueWorktree(17, f.root), /terminal/iu);
774
+ }
775
+ finally {
776
+ rmSync(f.root, { force: true, recursive: true });
777
+ }
778
+ });
779
+ test('terminal inspection rejects held removal and configured reviewer matching the native PR author', async () => {
780
+ for (const reason of ['held', 'reviewer-author']) {
781
+ const f = await transitionFixture({ purpose: 'merged-cleanup', disposition: 'remove', held: reason === 'held' });
782
+ try {
783
+ if (reason === 'reviewer-author')
784
+ f.nativePr.user.login = nativeAuthority.reviewer;
785
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
786
+ assert.equal(inspected.ready, false, reason);
787
+ assert.match(inspected.blockers.join('; '), reason === 'held' ? /disposition/iu : /native PR author/iu);
788
+ assert.equal(existsSync(join(gitCommonDir(f.root), 'ai-delivery/worktree-owners/issue-17.transition.json')), false);
789
+ }
790
+ finally {
791
+ rmSync(f.root, { force: true, recursive: true });
792
+ }
793
+ }
794
+ });
795
+ function entry(root, status = 'active') {
796
+ return {
797
+ branch: 'issue/17',
798
+ createdAt: '2026-01-01T00:00:00.000Z',
799
+ identity: 'synthetic-author',
800
+ issueNumber: 17,
801
+ path: join(root, '.worktrees', 'issue-17'),
802
+ status,
803
+ type: 'issue',
804
+ updatedAt: '2026-01-01T00:00:00.000Z',
805
+ };
806
+ }
807
+ function interruptTransition(root, row) {
808
+ // A truncated intent is also an incomplete transition and must fail closed.
809
+ const path = join(gitCommonDir(root), 'ai-delivery', 'worktree-owners', `issue-${String(row.issueNumber)}.transition.json`);
810
+ writeFileSync(path, '{', { mode: 0o600 });
811
+ }
812
+ test('ordinary ownership and resume refuse an interrupted transition even after the owner witness exists', async () => {
813
+ const root = repository();
814
+ try {
815
+ const row = entry(root);
816
+ await addWorktreeEntry(row, root);
817
+ assert.equal(getIssueWorktreeStrict(17, root).path, row.path);
818
+ interruptTransition(root, row);
819
+ assert.throws(() => getIssueWorktreeStrict(17, root), /Worktree transition.*(?:pending|incomplete|terminal)/iu);
820
+ await assert.rejects(prepareIssueWorktree({ identity: row.identity, issueNumber: 17, repoRoot: root }), /Worktree transition.*(?:pending|incomplete|terminal)/iu);
821
+ assert.deepEqual(listWorktreesStrict(root), [row]);
822
+ }
823
+ finally {
824
+ rmSync(root, { force: true, recursive: true });
825
+ }
826
+ });
827
+ test('registration cannot reactivate a merged issue through an interrupted transition', async () => {
828
+ const root = repository();
829
+ try {
830
+ const row = { ...entry(root, 'merged'), prNumber: 23 };
831
+ await addWorktreeEntry(row, root);
832
+ const registry = join(root, '.issue-cli', 'worktrees.json');
833
+ const original = readFileSync(registry);
834
+ interruptTransition(root, row);
835
+ await assert.rejects(addWorktreeEntry(entry(root), root), /Worktree transition.*(?:pending|incomplete|terminal)/iu);
836
+ assert.deepEqual(readFileSync(registry), original);
837
+ }
838
+ finally {
839
+ rmSync(root, { force: true, recursive: true });
840
+ }
841
+ });
842
+ test('ordinary ownership still refuses a legacy row without a witness', () => {
843
+ const root = repository();
844
+ try {
845
+ mkdirSync(join(root, '.issue-cli'), { mode: 0o700 });
846
+ writeFileSync(join(root, '.issue-cli', 'worktrees.json'), JSON.stringify({ worktrees: [entry(root)] }), {
847
+ mode: 0o600,
848
+ });
849
+ assert.throws(() => getIssueWorktreeStrict(17, root), /ownership witness/iu);
850
+ }
851
+ finally {
852
+ rmSync(root, { force: true, recursive: true });
853
+ }
854
+ });
855
+ test('direct tracking admission refuses pending intent after its canonical row is absent', async () => {
856
+ const root = repository();
857
+ try {
858
+ const row = entry(root);
859
+ await addWorktreeEntry(row, root);
860
+ writeFileSync(join(root, '.issue-cli/worktrees.json'), JSON.stringify({ worktrees: [] }), { mode: 0o600 });
861
+ interruptTransition(root, row);
862
+ assert.throws(() => assertNoForeignIssueWorktree(17, root), /Worktree transition.*(?:pending|incomplete|terminal)/iu);
863
+ await assert.rejects(prepareIssueWorktree({ identity: row.identity, issueNumber: 17, repoRoot: root }), /Worktree transition.*(?:pending|incomplete|terminal)/iu);
864
+ }
865
+ finally {
866
+ rmSync(root, { force: true, recursive: true });
867
+ }
868
+ });
869
+ test('absent-row admission refuses valid JSON with a non-issue or mismatched embedded row', async () => {
870
+ const root = repository();
871
+ try {
872
+ const row = entry(root);
873
+ await addWorktreeEntry(row, root);
874
+ writeFileSync(join(root, '.issue-cli/worktrees.json'), JSON.stringify({ worktrees: [] }), { mode: 0o600 });
875
+ const path = join(gitCommonDir(root), 'ai-delivery/worktree-owners/issue-17.transition.json');
876
+ for (const invalid of [{ type: 'pr' }, { ...row, issueNumber: 18 }, { ...row, issueNumber: undefined }]) {
877
+ writeFileSync(path, JSON.stringify({ plan: { row: invalid } }), { mode: 0o600 });
878
+ assert.throws(() => assertNoForeignIssueWorktree(17, root), /transition.*(?:incomplete|unreadable)/iu);
879
+ await assert.rejects(prepareIssueWorktree({ identity: row.identity, issueNumber: 17, repoRoot: root }), /transition.*(?:incomplete|unreadable)/iu);
880
+ }
881
+ }
882
+ finally {
883
+ rmSync(root, { recursive: true, force: true });
884
+ }
885
+ });
886
+ test('native acceptance cannot bypass terminal or active purpose and complete PR-set predicates', { timeout: 0 }, async () => {
887
+ for (const reason of ['empty-terminal-lineage', 'active-merged-lineage', 'active-merged-row']) {
888
+ const f = await transitionFixture({ purpose: 'merged-cleanup' });
889
+ try {
890
+ const inspected = await inspectWorktreeTransition(f.context, f.inspectInput);
891
+ assert.equal(inspected.ready, true, inspected.blockers.join('; '));
892
+ const { planId: _id, ...content } = inspected.plan;
893
+ if (reason === 'empty-terminal-lineage')
894
+ content.lineage = [];
895
+ else {
896
+ content.purpose = 'active-resume';
897
+ content.terminalPrNumber = null;
898
+ f.context.clients.rest.issues.get = (async () => ({
899
+ data: { state: 'open' },
900
+ }));
901
+ for (const id of [101, 102])
902
+ nativeAuthority.comments.get(id).issue_url = 'https://api.github.com/repos/example/widget/issues/17';
903
+ if (reason === 'active-merged-row') {
904
+ content.row = { ...content.row, status: 'merged' };
905
+ content.lineage = [];
906
+ writeFileSync(join(f.root, '.issue-cli/worktrees.json'), JSON.stringify({ worktrees: [content.row] }), {
907
+ mode: 0o600,
908
+ });
909
+ }
910
+ }
911
+ const plan = { ...content, planId: digestValue(content) };
912
+ await assert.rejects(applyWorktreeTransition(f.context, approvedFixturePlan(f, plan)), /lineage|active resume|terminal|PR.*set/iu, reason);
913
+ assert.equal(existsSync(join(gitCommonDir(f.root), 'ai-delivery/worktree-owners/issue-17.transition.json')), false);
914
+ }
915
+ finally {
916
+ rmSync(f.root, { recursive: true, force: true });
917
+ }
918
+ }
919
+ });
920
+ test('transition registry rejects a foreign issue row sharing its exact path or branch', async () => {
921
+ const root = repository();
922
+ try {
923
+ const { plan } = authorityFixture(root);
924
+ mkdirSync(join(root, '.issue-cli'), { recursive: true });
925
+ for (const conflict of ['path', 'branch']) {
926
+ const other = { ...entry(root), issueNumber: 18, path: join(root, '.worktrees', 'issue-18'), branch: 'issue/18' };
927
+ if (conflict === 'path')
928
+ other.path = plan.row.path;
929
+ else
930
+ other.branch = plan.row.branch;
931
+ writeFileSync(join(root, '.issue-cli/worktrees.json'), JSON.stringify({ worktrees: [plan.row, other] }), {
932
+ mode: 0o600,
933
+ });
934
+ await assert.rejects(withWorktreeTransitionRegistry(plan, async () => undefined), /colli|conflict/iu);
935
+ }
936
+ }
937
+ finally {
938
+ rmSync(root, { force: true, recursive: true });
939
+ }
940
+ });
941
+ test('historical inventory preserves unknown schemas and failed diagnostics byte for byte', () => {
942
+ const root = repository();
943
+ try {
944
+ const historical = join(root, '.issue-cli', 'issues', '17', 'review', 'historical-head');
945
+ mkdirSync(historical, { recursive: true });
946
+ const original = Buffer.from('{"schemaVersion":"historical.unknown@7","failed":true,"accounting":{"elapsed":51}}\n');
947
+ writeFileSync(join(historical, 'failed-diagnostic.json'), original);
948
+ const inventory = inventoryTransitionEvidence(root, entry(root));
949
+ const item = inventory.find((file) => file.path.endsWith('failed-diagnostic.json'));
950
+ assert.ok(item);
951
+ const output = join(gitCommonDir(root), 'ai-delivery', 'worktree-owners', 'preserved');
952
+ preserveTransitionEvidence(output, inventory);
953
+ assert.deepEqual(readFileSync(join(output, `${item.digest.slice(7)}.bin`)), original);
954
+ assert.deepEqual(readFileSync(join(historical, 'failed-diagnostic.json')), original);
955
+ }
956
+ finally {
957
+ rmSync(root, { force: true, recursive: true });
958
+ }
959
+ });
960
+ test('preservation rejects drift without replacing an already preserved original', () => {
961
+ const root = repository();
962
+ try {
963
+ const historical = join(root, '.issue-cli', 'issues', '17');
964
+ mkdirSync(historical, { recursive: true });
965
+ const source = join(historical, 'receipt.json');
966
+ writeFileSync(source, 'original historical bytes');
967
+ const inventory = inventoryTransitionEvidence(root, entry(root));
968
+ const output = join(gitCommonDir(root), 'ai-delivery', 'worktree-owners', 'preserved');
969
+ preserveTransitionEvidence(output, inventory);
970
+ writeFileSync(source, 'changed historical bytes');
971
+ assert.throws(() => preserveTransitionEvidence(output, inventory), /historical evidence.*drift/iu);
972
+ assert.deepEqual(readFileSync(join(output, `${inventory[0].digest.slice(7)}.bin`)), Buffer.from('original historical bytes'));
973
+ }
974
+ finally {
975
+ rmSync(root, { force: true, recursive: true });
976
+ }
977
+ });
978
+ test('inventory preserves worktree-local original receipts and accounting before terminal removal', () => {
979
+ const root = repository();
980
+ try {
981
+ const local = join(entry(root).path, '.issue-cli', 'receipts', 'verification');
982
+ mkdirSync(local, { recursive: true });
983
+ const path = join(local, 'historical.json');
984
+ const bytes = Buffer.from('{"schemaVersion":"legacy.receipt@2","failedDiagnostic":true,"accounting":{"runtimeMs":771}}\n');
985
+ writeFileSync(path, bytes);
986
+ const inventory = inventoryTransitionEvidence(root, entry(root));
987
+ const selected = inventory.find((file) => file.source === path);
988
+ assert.ok(selected, 'worktree-local original receipt was omitted');
989
+ const output = join(gitCommonDir(root), 'ai-delivery', 'worktree-owners', 'preserved');
990
+ preserveTransitionEvidence(output, inventory);
991
+ assert.deepEqual(readFileSync(join(output, `${selected.digest.slice(7)}.bin`)), bytes);
992
+ }
993
+ finally {
994
+ rmSync(root, { force: true, recursive: true });
995
+ }
996
+ });
997
+ function previousWriter(root, phase = 'idle') {
998
+ const worktree = join(root, '.worktrees', 'issue-17');
999
+ const worktreeDigest = digestValue(realpathSync(worktree));
1000
+ const content = {
1001
+ schemaVersion: 'ai-delivery.verification-writer@1',
1002
+ writerId: 'c9d58755-17b2-4fc9-9218-90f92c7642e1',
1003
+ worktreeDigest,
1004
+ owner: { pid: 2_000_000_000, identity: 'synthetic deceased process' },
1005
+ command: { phase },
1006
+ };
1007
+ const path = join(gitCommonDir(root), 'ai-delivery', 'writers@1', `${worktreeDigest.slice(7)}.json`);
1008
+ mkdirSync(join(gitCommonDir(root), 'ai-delivery', 'writers@1'), { mode: 0o700, recursive: true });
1009
+ const bytes = Buffer.from(`${JSON.stringify({ ...content, stateId: digestValue(content) }, null, 2)}\n`);
1010
+ writeFileSync(path, bytes, { mode: 0o600 });
1011
+ return { path, bytes };
1012
+ }
1013
+ test('transition lease preserves original bytes before claim and records release before deletion', async () => {
1014
+ const root = repository();
1015
+ try {
1016
+ const original = previousWriter(root);
1017
+ const events = [];
1018
+ const writerId = '1b75f48b-0746-43a5-bd6d-9ddc17848c9a';
1019
+ await withWorktreeTransitionWriterLease({
1020
+ repoRoot: entry(root).path,
1021
+ writerId,
1022
+ beforeClaim: ({ previousBytes }) => {
1023
+ assert.deepEqual(previousBytes, original.bytes);
1024
+ assert.deepEqual(readFileSync(original.path), original.bytes);
1025
+ events.push('preserved');
1026
+ },
1027
+ recordClaim: (bytes) => {
1028
+ assert.deepEqual(events, ['preserved']);
1029
+ assert.deepEqual(readFileSync(original.path), original.bytes);
1030
+ assert.equal(JSON.parse(bytes.toString()).writerId, writerId);
1031
+ events.push('claimed');
1032
+ },
1033
+ recordRelease: (bytes) => {
1034
+ assert.deepEqual(events, ['preserved', 'claimed', 'operation']);
1035
+ assert.deepEqual(readFileSync(original.path), bytes);
1036
+ events.push('released');
1037
+ },
1038
+ }, async () => {
1039
+ assert.equal(JSON.parse(readFileSync(original.path, 'utf8')).writerId, writerId);
1040
+ events.push('operation');
1041
+ });
1042
+ assert.deepEqual(events, ['preserved', 'claimed', 'operation', 'released']);
1043
+ assert.throws(() => readFileSync(original.path), { code: 'ENOENT' });
1044
+ }
1045
+ finally {
1046
+ rmSync(root, { force: true, recursive: true });
1047
+ }
1048
+ });
1049
+ test('failed intent preservation cannot overwrite the original writer', async () => {
1050
+ const root = repository();
1051
+ try {
1052
+ const original = previousWriter(root);
1053
+ await assert.rejects(withWorktreeTransitionWriterLease({
1054
+ repoRoot: entry(root).path,
1055
+ writerId: '1b75f48b-0746-43a5-bd6d-9ddc17848c9a',
1056
+ beforeClaim: () => {
1057
+ throw new Error('synthetic intent failure');
1058
+ },
1059
+ recordClaim: () => assert.fail('claim must not be reached'),
1060
+ recordRelease: () => assert.fail('release must not be reached'),
1061
+ }, async () => assert.fail('operation must not be reached')), /synthetic intent failure/u);
1062
+ assert.deepEqual(readFileSync(original.path), original.bytes);
1063
+ }
1064
+ finally {
1065
+ rmSync(root, { force: true, recursive: true });
1066
+ }
1067
+ });
1068
+ test('failed claim evidence cannot replace the preserved writer', async () => {
1069
+ const root = repository();
1070
+ try {
1071
+ const original = previousWriter(root);
1072
+ await assert.rejects(withWorktreeTransitionWriterLease({
1073
+ repoRoot: entry(root).path,
1074
+ writerId: '1b75f48b-0746-43a5-bd6d-9ddc17848c9a',
1075
+ beforeClaim: () => undefined,
1076
+ recordClaim: () => {
1077
+ throw new Error('synthetic claim failure');
1078
+ },
1079
+ recordRelease: () => assert.fail('release must not be reached'),
1080
+ }, async () => assert.fail('operation must not be reached')), /synthetic claim failure/u);
1081
+ assert.deepEqual(readFileSync(original.path), original.bytes);
1082
+ }
1083
+ finally {
1084
+ rmSync(root, { force: true, recursive: true });
1085
+ }
1086
+ });
1087
+ test('transition refuses an interrupted unknown launch without changing its state', async () => {
1088
+ const root = repository();
1089
+ try {
1090
+ const original = previousWriter(root, 'starting');
1091
+ await assert.rejects(withWorktreeTransitionWriterLease({
1092
+ repoRoot: entry(root).path,
1093
+ writerId: '1b75f48b-0746-43a5-bd6d-9ddc17848c9a',
1094
+ beforeClaim: () => assert.fail('unknown ownership must refuse before preservation'),
1095
+ recordClaim: () => assert.fail('unknown ownership must refuse before claim'),
1096
+ recordRelease: () => assert.fail('unknown ownership must refuse before release'),
1097
+ }, async () => assert.fail('operation must not be reached')), /unknown command ownership/u);
1098
+ assert.deepEqual(readFileSync(original.path), original.bytes);
1099
+ }
1100
+ finally {
1101
+ rmSync(root, { force: true, recursive: true });
1102
+ }
1103
+ });
1104
+ test('transition cannot delete its writer when durable release evidence fails', async () => {
1105
+ const root = repository();
1106
+ try {
1107
+ const original = previousWriter(root);
1108
+ let claim;
1109
+ await assert.rejects(withWorktreeTransitionWriterLease({
1110
+ repoRoot: entry(root).path,
1111
+ writerId: '1b75f48b-0746-43a5-bd6d-9ddc17848c9a',
1112
+ beforeClaim: () => undefined,
1113
+ recordClaim: (bytes) => {
1114
+ claim = Buffer.from(bytes);
1115
+ },
1116
+ recordRelease: () => {
1117
+ throw new Error('synthetic release evidence failure');
1118
+ },
1119
+ }, async () => undefined), /synthetic release evidence failure/u);
1120
+ assert.deepEqual(readFileSync(original.path), claim);
1121
+ }
1122
+ finally {
1123
+ rmSync(root, { force: true, recursive: true });
1124
+ }
1125
+ });
1126
+ test('transition seals completion only after recorded writer deletion while the lease remains held', async () => {
1127
+ const root = repository();
1128
+ try {
1129
+ const original = previousWriter(root);
1130
+ let releaseRecorded = false;
1131
+ let sealed = false;
1132
+ await withWorktreeTransitionWriterLease({
1133
+ repoRoot: entry(root).path,
1134
+ writerId: '1b75f48b-0746-43a5-bd6d-9ddc17848c9a',
1135
+ beforeClaim: () => undefined,
1136
+ recordClaim: () => undefined,
1137
+ recordRelease: () => {
1138
+ releaseRecorded = true;
1139
+ },
1140
+ afterRelease: () => {
1141
+ assert.equal(releaseRecorded, true);
1142
+ assert.throws(() => readFileSync(original.path), { code: 'ENOENT' });
1143
+ assert.equal(realpathSync(`${original.path}.lock`), `${original.path}.lock`);
1144
+ sealed = true;
1145
+ },
1146
+ }, async () => undefined);
1147
+ assert.equal(sealed, true);
1148
+ assert.throws(() => realpathSync(`${original.path}.lock`), { code: 'ENOENT' });
1149
+ }
1150
+ finally {
1151
+ rmSync(root, { force: true, recursive: true });
1152
+ }
1153
+ });
1154
+ test('removed-worktree replay uses the original subject writer slot without recreating source', async () => {
1155
+ const root = repository();
1156
+ try {
1157
+ const row = entry(root);
1158
+ const original = previousWriter(root);
1159
+ execFileSync('git', ['worktree', 'remove', row.path], { cwd: root, stdio: 'pipe' });
1160
+ let captured = false;
1161
+ await withWorktreeTransitionWriterLease({
1162
+ repoRoot: root,
1163
+ worktreePath: row.path,
1164
+ writerId: '1b75f48b-0746-43a5-bd6d-9ddc17848c9a',
1165
+ beforeClaim: ({ previousBytes }) => {
1166
+ assert.deepEqual(previousBytes, original.bytes);
1167
+ captured = true;
1168
+ },
1169
+ recordClaim: () => undefined,
1170
+ recordRelease: () => undefined,
1171
+ }, async () => assert.throws(() => realpathSync(row.path), { code: 'ENOENT' }));
1172
+ assert.equal(captured, true);
1173
+ assert.throws(() => realpathSync(row.path), { code: 'ENOENT' });
1174
+ }
1175
+ finally {
1176
+ rmSync(root, { force: true, recursive: true });
1177
+ }
1178
+ });
1179
+ //# sourceMappingURL=worktreeTransition.test.js.map