@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
@@ -2792,9 +2792,12 @@ test('MCP exposes one implementation surface for native lifecycle commands', ()
2792
2792
  'issue_pr_merge',
2793
2793
  'issue_finish',
2794
2794
  'issue_worktree_create',
2795
+ 'issue_worktree_transition_inspect',
2796
+ 'issue_worktree_transition_apply',
2795
2797
  ]);
2796
2798
  const validInputs = {
2797
2799
  issue_create: { title: 'Synthetic tracking parent' },
2800
+ issue_update: { issueNumber: 17, park: true },
2798
2801
  issue_ready_check: { issueNumber: 17 },
2799
2802
  issue_develop: { issueNumber: 17 },
2800
2803
  issue_verify: { issueNumber: 17 },
@@ -2802,7 +2805,17 @@ test('MCP exposes one implementation surface for native lifecycle commands', ()
2802
2805
  issue_pr_review: { issueNumber: 17, prNumber: 23, artifact: '{}' },
2803
2806
  issue_pr_merge: { issueNumber: 17, prNumber: 23, strategy: 'merge' },
2804
2807
  issue_finish: { issueNumber: 17, prNumber: 23, strategy: 'merge' },
2808
+ issue_worktree_transition_inspect: { issueNumber: 17, purpose: 'active-resume' },
2809
+ issue_worktree_transition_apply: {
2810
+ authority: 'worktree:transition',
2811
+ planPath: '/synthetic/plan.json',
2812
+ expectedPlanId: `sha256:${'a'.repeat(64)}`,
2813
+ relinquishmentCommentId: 101,
2814
+ acceptanceCommentId: 102,
2815
+ },
2805
2816
  };
2817
+ const updateTool = AI_DELIVERY_MCP_TOOLS.find((tool) => tool.name === 'issue_update');
2818
+ assert.equal(updateTool.inputSchema.safeParse({ issueNumber: 17, park: false }).success, false);
2806
2819
  for (const [name, input] of Object.entries(validInputs)) {
2807
2820
  const tool = AI_DELIVERY_MCP_TOOLS.find((candidate) => candidate.name === name);
2808
2821
  assert.ok(tool, `${name} has an MCP definition`);
@@ -2870,6 +2883,9 @@ async function syntheticLifecycle(routing) {
2870
2883
  let trackingParent = false;
2871
2884
  let failFirstBlockerLink = true;
2872
2885
  let failNextProjectSync = false;
2886
+ let interruptNextStatusUpdate = false;
2887
+ let conflictNextStatusReadback = false;
2888
+ let projectStatusWrites = 0;
2873
2889
  let checksPassed = true;
2874
2890
  let prDraft = true;
2875
2891
  let prState = 'open';
@@ -2976,6 +2992,12 @@ async function syntheticLifecycle(routing) {
2976
2992
  nodes: projectStatus === null
2977
2993
  ? []
2978
2994
  : [
2995
+ {
2996
+ id: 'UNRELATED-ITEM',
2997
+ isArchived: false,
2998
+ content: { id: 'UNRELATED-ISSUE' },
2999
+ fieldValueByName: { name: 'Active', optionId: 'STATUS-1' },
3000
+ },
2979
3001
  {
2980
3002
  id: 'ITEM-17',
2981
3003
  isArchived: false,
@@ -2999,19 +3021,29 @@ async function syntheticLifecycle(routing) {
2999
3021
  return { addProjectV2ItemById: { item: { id: 'ITEM-17' } } };
3000
3022
  }
3001
3023
  if (query.includes('UpdateProjectDeliveryStatus')) {
3024
+ assert.equal(variables.projectId, 'PROJECT-1');
3025
+ assert.equal(variables.itemId, 'ITEM-17');
3026
+ projectStatusWrites += 1;
3002
3027
  projectStatus = statusName(String(variables.optionId)) ?? null;
3028
+ if (interruptNextStatusUpdate) {
3029
+ interruptNextStatusUpdate = false;
3030
+ throw new Error('synthetic ambiguous status write');
3031
+ }
3003
3032
  return { updateProjectV2ItemFieldValue: { projectV2Item: { id: 'ITEM-17' } } };
3004
3033
  }
3005
- if (query.includes('ProjectDeliveryItemReadback'))
3034
+ if (query.includes('ProjectDeliveryItemReadback')) {
3035
+ const contentId = conflictNextStatusReadback ? 'UNRELATED-ISSUE' : issue.node_id;
3036
+ conflictNextStatusReadback = false;
3006
3037
  return {
3007
3038
  node: {
3008
3039
  id: 'ITEM-17',
3009
3040
  isArchived: false,
3010
3041
  project: { id: 'PROJECT-1', number: 1 },
3011
- content: { id: issue.node_id },
3042
+ content: { id: contentId },
3012
3043
  fieldValueByName: { name: projectStatus, optionId: statuses[projectStatus] },
3013
3044
  },
3014
3045
  };
3046
+ }
3015
3047
  if (query.includes('blockedBy(first:'))
3016
3048
  return {
3017
3049
  repository: {
@@ -3032,6 +3064,10 @@ async function syntheticLifecycle(routing) {
3032
3064
  blockerOpen = true;
3033
3065
  return { addBlockedBy: { issue: { number: issueNumber } } };
3034
3066
  }
3067
+ if (query.includes('removeBlockedBy')) {
3068
+ blockerOpen = false;
3069
+ return { removeBlockedBy: { issue: { number: issueNumber } } };
3070
+ }
3035
3071
  if (query.includes('RepositoryId'))
3036
3072
  return { repository: { id: 'REPO-1' } };
3037
3073
  if (query.includes('UpdateExactRefs')) {
@@ -3328,6 +3364,41 @@ async function syntheticLifecycle(routing) {
3328
3364
  assert.equal(started.title, issue.title);
3329
3365
  const row = started;
3330
3366
  assert.equal(projectStatus, 'Active');
3367
+ const parked = await updateIssue(context, { issueNumber, park: true });
3368
+ assert.equal(parked.projectStatus, 'Todo');
3369
+ assert.equal(existsSync(row.path), true);
3370
+ assert.equal((await updateIssue(context, { issueNumber, title: issue.title })).projectStatus, 'Todo');
3371
+ await developIssue(context, issueNumber);
3372
+ assert.equal(projectStatus, 'Active');
3373
+ const blockedUpdate = await updateIssue(context, { issueNumber, blockedBy: [9] });
3374
+ assert.deepEqual(blockedUpdate.blockedBy, [9]);
3375
+ assert.equal(blockedUpdate.projectStatus, 'Blocked');
3376
+ assert.equal(projectStatus, 'Waiting');
3377
+ const blockedWrites = projectStatusWrites;
3378
+ assert.equal((await updateIssue(context, { issueNumber, blockedBy: [9] })).projectStatus, 'Blocked');
3379
+ assert.equal(projectStatusWrites, blockedWrites);
3380
+ assert.equal((await updateIssue(context, { issueNumber, park: true })).projectStatus, 'Blocked');
3381
+ await assert.rejects(developIssue(context, issueNumber), /not ready/u);
3382
+ interruptNextStatusUpdate = true;
3383
+ await assert.rejects(updateIssue(context, { issueNumber, blockedBy: [] }), /ambiguous status write/u);
3384
+ assert.equal(blockerOpen, false);
3385
+ assert.equal(projectStatus, 'Queued');
3386
+ const interruptedWrites = projectStatusWrites;
3387
+ const unblockedUpdate = await updateIssue(context, { issueNumber, blockedBy: [] });
3388
+ assert.equal(projectStatusWrites, interruptedWrites);
3389
+ assert.deepEqual(unblockedUpdate.blockedBy, []);
3390
+ assert.equal(unblockedUpdate.projectStatus, 'Todo');
3391
+ conflictNextStatusReadback = true;
3392
+ await assert.rejects(updateIssue(context, { issueNumber, blockedBy: [9] }), /exact issue item/u);
3393
+ assert.equal(blockerOpen, true);
3394
+ assert.equal((await updateIssue(context, { issueNumber, blockedBy: [9] })).projectStatus, 'Blocked');
3395
+ assert.equal((await updateIssue(context, { issueNumber, state: 'closed' })).projectStatus, 'Done');
3396
+ assert.equal((await updateIssue(context, { issueNumber, park: true })).projectStatus, 'Done');
3397
+ assert.equal((await resumeCreatedIssue(context, { issueNumber })).projectStatus, 'Done');
3398
+ assert.equal((await updateIssue(context, { issueNumber, state: 'open', blockedBy: [] })).projectStatus, 'Done');
3399
+ assert.equal(existsSync(row.path), true);
3400
+ await developIssue(context, issueNumber);
3401
+ assert.equal(projectStatus, 'Active');
3331
3402
  writeFileSync(join(row.path, 'change.txt'), 'feature\n');
3332
3403
  git(row.path, 'add', 'change.txt');
3333
3404
  git(row.path, 'commit', '-qm', 'synthetic change');
@@ -3550,9 +3621,9 @@ test.each([
3550
3621
  { remote: 'upstream', divergentOrigin: false },
3551
3622
  { remote: 'upstream', divergentOrigin: true },
3552
3623
  ])('synthetic lifecycle finishes with remote $remote and divergent origin $divergentOrigin', syntheticLifecycle, 15_000);
3553
- test('an unrelated consumer uses public MCP setup through verified configured-author publication, counted review readback, merge and cleanup', async () => {
3624
+ test('an unrelated consumer uses public MCP setup through verified configured-author publication, counted review readback, merge and cleanup', { timeout: 0 }, async () => {
3554
3625
  await syntheticLifecycle({ remote: 'origin', divergentOrigin: false, producerOnboarding: true });
3555
- }, 15_000);
3626
+ });
3556
3627
  test.each(['escape', 'broken', 'cycle'])('output observation rejects %s aliases without running policy commands', async (kind) => {
3557
3628
  const { root, counter } = await fixture();
3558
3629
  const base = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-output-alias-')));
@@ -3592,4 +3663,399 @@ test.each(['escape', 'broken', 'cycle'])('output observation rejects %s aliases
3592
3663
  rmSync(base, { recursive: true, force: true });
3593
3664
  }
3594
3665
  });
3666
+ test('fully cached bounded verification still rejects a newly leaked FIFO', async () => {
3667
+ const { root, counter } = await fixture();
3668
+ const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-cached-fifo-')));
3669
+ try {
3670
+ const row = await prepareIssueWorktree({
3671
+ baseRef: 'main',
3672
+ identity: 'synthetic-author',
3673
+ issueNumber: 17,
3674
+ repoRoot: root,
3675
+ });
3676
+ const bounds = {
3677
+ maxAggregateRssBytes: 1_000_000_000,
3678
+ minFreeDiskBytes: 1,
3679
+ maxNewOutputBytes: 1024,
3680
+ outputRoots: [output],
3681
+ };
3682
+ await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds });
3683
+ assert.equal(readFileSync(counter, 'utf8'), '1');
3684
+ execFileSync('mkfifo', [join(output, 'leaked')]);
3685
+ await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds }), /unsupported file/u);
3686
+ assert.equal(readFileSync(counter, 'utf8'), '1');
3687
+ }
3688
+ finally {
3689
+ rmSync(output, { recursive: true, force: true });
3690
+ rmSync(root, { recursive: true, force: true });
3691
+ }
3692
+ });
3693
+ function filesystemFixtureScript(output, mode) {
3694
+ return `
3695
+ (async () => {
3696
+ const fs = require('node:fs'), cp = require('node:child_process'), path = require('node:path'), assert = require('node:assert/strict');
3697
+ const { withVerificationFilesystemFixture } = await import(${JSON.stringify(new URL('./agent.js', import.meta.url).href)});
3698
+ const { assertPrivateFile } = await import(${JSON.stringify(new URL('./delivery/common.js', import.meta.url).href)});
3699
+ const output = ${JSON.stringify(output)}, fifo = path.join(output, 'fifo'), events = path.join(output, 'events');
3700
+ const gate = JSON.parse(process.env.AI_DELIVERY_OUTPUT_OBSERVATION_GATE);
3701
+ const trace = value => fs.appendFileSync(events, value + '\\n');
3702
+ await withVerificationFilesystemFixture(async () => {
3703
+ cp.execFileSync('mkfifo', ['-m', '600', fifo]);
3704
+ fs.writeFileSync(path.join(output, 'capability'), JSON.stringify(gate));
3705
+ trace('created');
3706
+ try {
3707
+ if (${JSON.stringify(mode)} === 'abrupt') process.exit(9);
3708
+ if (${JSON.stringify(mode)} === 'cancel') await new Promise(() => { setInterval(() => {}, 1000); });
3709
+ if (${JSON.stringify(mode)} === 'block') fs.readFileSync(fifo);
3710
+ while (fs.readdirSync(path.join(gate.directory, 'requests')).length === 0) await new Promise(resolve => setTimeout(resolve, 10));
3711
+ assert.ok(fs.existsSync(fifo)); trace('scan-queued');
3712
+ assert.throws(() => assertPrivateFile(fifo), /private regular file/); trace('reader-rejected');
3713
+ const regular = path.join(output, 'regular'), hard = path.join(output, 'hard'), alias = path.join(output, 'alias');
3714
+ fs.writeFileSync(regular, 'x'.repeat(128), { mode: 0o600 });
3715
+ fs.linkSync(regular, hard); assert.throws(() => assertPrivateFile(hard), /private regular file/); fs.unlinkSync(hard);
3716
+ fs.symlinkSync('./regular', alias); assert.throws(() => assertPrivateFile(alias), /private regular file/); fs.unlinkSync(alias);
3717
+ if (${JSON.stringify(mode)} === 'assertion') throw new Error('synthetic reader assertion failure');
3718
+ } finally { fs.unlinkSync(fifo); trace('cleaned'); }
3719
+ });
3720
+ trace('released');
3721
+ })().catch(error => { console.error(error.message); process.exitCode = 1; });`;
3722
+ }
3723
+ test.each(['pass', 'assertion'])('filesystem fixture gate deterministically queues a scan through reader rejection and %s teardown', async (mode) => {
3724
+ const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-gate-')));
3725
+ const { root } = await fixture({ firstStageScript: filesystemFixtureScript(output, mode) });
3726
+ try {
3727
+ const row = await prepareIssueWorktree({
3728
+ baseRef: 'main',
3729
+ identity: 'synthetic-author',
3730
+ issueNumber: 17,
3731
+ repoRoot: root,
3732
+ });
3733
+ const run = verifyIssue({
3734
+ issueNumber: 17,
3735
+ repoRoot: row.path,
3736
+ resourceBounds: {
3737
+ maxAggregateRssBytes: 1_000_000_000,
3738
+ minFreeDiskBytes: 1,
3739
+ maxNewOutputBytes: 2048,
3740
+ outputRoots: [output],
3741
+ },
3742
+ });
3743
+ if (mode === 'assertion')
3744
+ await assert.rejects(run, /exit 1/u);
3745
+ else
3746
+ assert.ok((await run).resources.maxSampledNewOutputBytes >= 128);
3747
+ assert.equal(existsSync(join(output, 'fifo')), false);
3748
+ const events = readFileSync(join(output, 'events'), 'utf8').trim().split('\n');
3749
+ assert.deepEqual(events, mode === 'pass'
3750
+ ? ['created', 'scan-queued', 'reader-rejected', 'cleaned', 'released']
3751
+ : ['created', 'scan-queued', 'reader-rejected', 'cleaned']);
3752
+ const capability = JSON.parse(readFileSync(join(output, 'capability'), 'utf8'));
3753
+ assert.equal(existsSync(capability.directory), false);
3754
+ assert.equal(readFileSync(join(output, 'regular'), 'utf8').length, 128);
3755
+ }
3756
+ finally {
3757
+ rmSync(root, { recursive: true, force: true });
3758
+ rmSync(output, { recursive: true, force: true });
3759
+ }
3760
+ });
3761
+ test.each(['abrupt', 'block', 'cancel'])('filesystem fixture gate preserves leaked FIFO and unrelated files after %s', async (mode) => {
3762
+ const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-failure-')));
3763
+ writeFileSync(join(output, 'unrelated'), 'preserved');
3764
+ const { root } = await fixture({ firstStageScript: filesystemFixtureScript(output, mode) });
3765
+ const controller = new AbortController();
3766
+ let running;
3767
+ const progress = vi.spyOn(process.stderr, 'write');
3768
+ try {
3769
+ const row = await prepareIssueWorktree({
3770
+ baseRef: 'main',
3771
+ identity: 'synthetic-author',
3772
+ issueNumber: 17,
3773
+ repoRoot: root,
3774
+ });
3775
+ const bounds = {
3776
+ maxAggregateRssBytes: 1_000_000_000,
3777
+ minFreeDiskBytes: 1,
3778
+ maxNewOutputBytes: 2048,
3779
+ outputRoots: [output],
3780
+ };
3781
+ running = verifyIssue({
3782
+ issueNumber: 17,
3783
+ repoRoot: row.path,
3784
+ resourceBounds: bounds,
3785
+ signal: controller.signal,
3786
+ }).catch((error) => error);
3787
+ if (mode === 'cancel') {
3788
+ for (let attempt = 0; attempt < 300 && !existsSync(join(output, 'capability')); attempt++)
3789
+ await new Promise((resolve) => setTimeout(resolve, 20));
3790
+ assert.ok(existsSync(join(output, 'capability')));
3791
+ const cap = JSON.parse(readFileSync(join(output, 'capability'), 'utf8'));
3792
+ for (let attempt = 0; attempt < 300 && readdirSync(join(cap.directory, 'requests')).length === 0; attempt++)
3793
+ await new Promise((resolve) => setTimeout(resolve, 20));
3794
+ assert.ok(readdirSync(join(cap.directory, 'requests')).length > 0);
3795
+ controller.abort();
3796
+ }
3797
+ const failure = String(await running);
3798
+ assert.match(failure, mode === 'abrupt' ? /exit 9/u : mode === 'block' ? /reader or teardown is stalled/u : /cancelled/u);
3799
+ assert.doesNotMatch(failure, /cleanup failed|live holder/u);
3800
+ const capability = JSON.parse(readFileSync(join(output, 'capability'), 'utf8'));
3801
+ assert.equal(existsSync(capability.directory), false);
3802
+ assert.ok(existsSync(join(output, 'fifo')));
3803
+ assert.equal(readFileSync(join(output, 'unrelated'), 'utf8'), 'preserved');
3804
+ if (mode !== 'abrupt')
3805
+ assert.ok(progress.mock.calls.filter(([line]) => String(line).includes('sampledAggregateRssBytes')).length > 2);
3806
+ await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds }), /unsupported file/u);
3807
+ }
3808
+ finally {
3809
+ controller.abort();
3810
+ await running;
3811
+ progress.mockRestore();
3812
+ rmSync(root, { recursive: true, force: true });
3813
+ rmSync(output, { recursive: true, force: true });
3814
+ }
3815
+ });
3816
+ test('filesystem fixture gate still enforces positive byte limits after deterministic contention', async () => {
3817
+ const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-limit-')));
3818
+ const { root } = await fixture({ firstStageScript: filesystemFixtureScript(output, 'pass') });
3819
+ try {
3820
+ const row = await prepareIssueWorktree({
3821
+ baseRef: 'main',
3822
+ identity: 'synthetic-author',
3823
+ issueNumber: 17,
3824
+ repoRoot: root,
3825
+ });
3826
+ await assert.rejects(verifyIssue({
3827
+ issueNumber: 17,
3828
+ repoRoot: row.path,
3829
+ resourceBounds: {
3830
+ maxAggregateRssBytes: 1_000_000_000,
3831
+ minFreeDiskBytes: 1,
3832
+ maxNewOutputBytes: 64,
3833
+ outputRoots: [output],
3834
+ },
3835
+ }), /filesystem output.*exceeded/u);
3836
+ assert.equal(existsSync(join(output, 'fifo')), false);
3837
+ assert.equal(readFileSync(join(output, 'regular'), 'utf8').length, 128);
3838
+ }
3839
+ finally {
3840
+ rmSync(root, { recursive: true, force: true });
3841
+ rmSync(output, { recursive: true, force: true });
3842
+ }
3843
+ });
3844
+ test.each(['verification', 'runtime'])('filesystem fixture gate preserves observation and cancellation after direct %s command closure', async (mode) => {
3845
+ const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-close-')));
3846
+ writeFileSync(join(output, 'unrelated'), 'preserved');
3847
+ const script = `
3848
+ (async () => {
3849
+ const fs = require('node:fs'), cp = require('node:child_process');
3850
+ const child = cp.spawn(process.execPath, ['-e', ${JSON.stringify(filesystemFixtureScript(output, 'cancel'))}], { stdio: 'ignore' });
3851
+ child.unref();
3852
+ fs.writeFileSync(${JSON.stringify(join(output, 'pids'))}, JSON.stringify({ root: process.pid, descendant: child.pid }));
3853
+ while (!fs.existsSync(${JSON.stringify(join(output, 'exit-parent'))})) await new Promise(resolve => setTimeout(resolve, 10));
3854
+ process.exit(0);
3855
+ })().catch(error => { console.error(error.message); process.exitCode = 1; });`;
3856
+ const { root } = await fixture({ firstStageScript: script });
3857
+ const controller = new AbortController();
3858
+ const progress = vi.spyOn(process.stderr, 'write');
3859
+ let running;
3860
+ try {
3861
+ const row = await prepareIssueWorktree({
3862
+ baseRef: 'main',
3863
+ identity: 'synthetic-author',
3864
+ issueNumber: 17,
3865
+ repoRoot: root,
3866
+ });
3867
+ const bounds = {
3868
+ maxAggregateRssBytes: 1_000_000_000,
3869
+ minFreeDiskBytes: 1,
3870
+ maxNewOutputBytes: 4096,
3871
+ outputRoots: [output],
3872
+ };
3873
+ const { withRuntimeSetupWriter } = await import('./verification.js');
3874
+ running = (mode === 'verification'
3875
+ ? verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds, signal: controller.signal })
3876
+ : withRuntimeSetupWriter(row.path, (runner) => runner.run([process.execPath, '-e', script], bounds, controller.signal))).catch((error) => error);
3877
+ for (let attempt = 0; attempt < 300 && !existsSync(join(output, 'capability')); attempt++)
3878
+ await new Promise((resolve) => setTimeout(resolve, 20));
3879
+ assert.ok(existsSync(join(output, 'capability')));
3880
+ const cap = JSON.parse(readFileSync(join(output, 'capability'), 'utf8'));
3881
+ const pids = JSON.parse(readFileSync(join(output, 'pids'), 'utf8'));
3882
+ const writerPath = join(root, '.git', 'ai-delivery', 'writers@1', digestValue(row.path).slice(7) + '.json');
3883
+ const descendantObserved = () => {
3884
+ const state = JSON.parse(readFileSync(writerPath, 'utf8'));
3885
+ return state.command.tracked?.some((member) => member.pid === pids.descendant) === true;
3886
+ };
3887
+ for (let attempt = 0; attempt < 300 && (!descendantObserved() || readdirSync(join(cap.directory, 'requests')).length === 0); attempt++)
3888
+ await new Promise((resolve) => setTimeout(resolve, 20));
3889
+ assert.ok(descendantObserved());
3890
+ assert.ok(readdirSync(join(cap.directory, 'requests')).length > 0);
3891
+ writeFileSync(join(output, 'exit-parent'), 'exit');
3892
+ const parentAlive = () => spawnSync('ps', ['-p', String(pids.root), '-o', 'stat='], { encoding: 'utf8' }).stdout.trim().length > 0;
3893
+ for (let attempt = 0; attempt < 100 && parentAlive(); attempt++)
3894
+ await new Promise((resolve) => setTimeout(resolve, 20));
3895
+ assert.equal(parentAlive(), false);
3896
+ const samples = () => progress.mock.calls.filter(([line]) => String(line).includes('sampledAggregateRssBytes')).length;
3897
+ const afterExit = samples();
3898
+ for (let attempt = 0; attempt < 100 && samples() === afterExit; attempt++)
3899
+ await new Promise((resolve) => setTimeout(resolve, 20));
3900
+ const observedAfterExit = samples() > afterExit;
3901
+ controller.abort();
3902
+ const failure = String(await running);
3903
+ assert.match(failure, /cancelled/u);
3904
+ assert.doesNotMatch(failure, /cleanup failed|live holder/u);
3905
+ assert.ok(observedAfterExit, 'RSS and disk observation continues while the closed command awaits its scan');
3906
+ assert.equal(existsSync(cap.directory), false);
3907
+ assert.ok(existsSync(join(output, 'fifo')));
3908
+ assert.equal(readFileSync(join(output, 'unrelated'), 'utf8'), 'preserved');
3909
+ assert.equal(spawnSync('ps', ['-p', String(pids.descendant), '-o', 'stat='], { encoding: 'utf8' }).stdout.trim().length, 0);
3910
+ assert.equal(progress.mock.calls.some(([line]) => String(line).includes('"state":"completed"')), false);
3911
+ }
3912
+ finally {
3913
+ controller.abort();
3914
+ await running;
3915
+ progress.mockRestore();
3916
+ rmSync(root, { recursive: true, force: true });
3917
+ rmSync(output, { recursive: true, force: true });
3918
+ }
3919
+ });
3920
+ test('filesystem fixture helper runs standalone and fails closed on malformed capability', async () => {
3921
+ const { withVerificationFilesystemFixture } = await import('./agent.js');
3922
+ assert.equal(await withVerificationFilesystemFixture(() => 'standalone'), 'standalone');
3923
+ const prior = process.env['AI_DELIVERY_OUTPUT_OBSERVATION_GATE'];
3924
+ try {
3925
+ process.env['AI_DELIVERY_OUTPUT_OBSERVATION_GATE'] = '{}';
3926
+ let executed = false;
3927
+ await assert.rejects(withVerificationFilesystemFixture(() => {
3928
+ executed = true;
3929
+ }), /invalid or stale/u);
3930
+ assert.equal(executed, false);
3931
+ }
3932
+ finally {
3933
+ if (prior === undefined)
3934
+ delete process.env['AI_DELIVERY_OUTPUT_OBSERVATION_GATE'];
3935
+ else
3936
+ process.env['AI_DELIVERY_OUTPUT_OBSERVATION_GATE'] = prior;
3937
+ }
3938
+ });
3939
+ test('nested runtime setup inherits the outer gate for its baseline and child command', async () => {
3940
+ const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-nested-fixture-')));
3941
+ const inner = await fixture();
3942
+ const worker = filesystemFixtureScript(output, 'pass');
3943
+ const { root } = await fixture({
3944
+ firstStageScript: `
3945
+ (async () => {
3946
+ const fs = require('node:fs'), cp = require('node:child_process'), assert = require('node:assert/strict');
3947
+ const { withRuntimeSetupWriter } = await import(${JSON.stringify(new URL('./verification.js', import.meta.url).href)});
3948
+ const capability = process.env.AI_DELIVERY_OUTPUT_OBSERVATION_GATE;
3949
+ const child = cp.spawn(process.execPath, ['-e', ${JSON.stringify(worker)}], { stdio: ['ignore', 'inherit', 'inherit'] });
3950
+ const closed = new Promise((resolve, reject) => child.once('exit', code => code === 0 ? resolve() : reject(new Error('fixture child exit ' + code))));
3951
+ while (!fs.existsSync(${JSON.stringify(join(output, 'capability'))})) await new Promise(resolve => setTimeout(resolve, 10));
3952
+ assert.equal(fs.readFileSync(${JSON.stringify(join(output, 'capability'))}, 'utf8'), capability);
3953
+ await withRuntimeSetupWriter(${JSON.stringify(inner.root)}, async runner => {
3954
+ await runner.run([process.execPath, '-e', 'require("node:assert/strict").equal(process.env.AI_DELIVERY_OUTPUT_OBSERVATION_GATE, ' + JSON.stringify(capability) + ')'], { maxAggregateRssBytes: 1000000000, minFreeDiskBytes: 1, maxNewOutputBytes: 2048, outputRoots: [${JSON.stringify(output)}] });
3955
+ });
3956
+ await closed;
3957
+ })().catch(error => { console.error(error.message); process.exitCode = 1; });`,
3958
+ });
3959
+ try {
3960
+ const row = await prepareIssueWorktree({
3961
+ baseRef: 'main',
3962
+ identity: 'synthetic-author',
3963
+ issueNumber: 17,
3964
+ repoRoot: root,
3965
+ });
3966
+ await verifyIssue({
3967
+ issueNumber: 17,
3968
+ repoRoot: row.path,
3969
+ resourceBounds: {
3970
+ maxAggregateRssBytes: 1_000_000_000,
3971
+ minFreeDiskBytes: 1,
3972
+ maxNewOutputBytes: 2048,
3973
+ outputRoots: [output],
3974
+ },
3975
+ });
3976
+ assert.deepEqual(readFileSync(join(output, 'events'), 'utf8').trim().split('\n'), [
3977
+ 'created',
3978
+ 'scan-queued',
3979
+ 'reader-rejected',
3980
+ 'cleaned',
3981
+ 'released',
3982
+ ]);
3983
+ assert.equal(existsSync(join(inner.root, '.git', 'ai-delivery', 'output-observation')), false);
3984
+ }
3985
+ finally {
3986
+ rmSync(root, { recursive: true, force: true });
3987
+ rmSync(inner.root, { recursive: true, force: true });
3988
+ rmSync(output, { recursive: true, force: true });
3989
+ }
3990
+ });
3991
+ test('interrupted fixture owner recovers its gate, preserves completed work and rejects a leaked FIFO before resume', async () => {
3992
+ const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-recovery-')));
3993
+ const marker = join(output, 'capability');
3994
+ const { root, counter, secondCounter, failSecond } = await fixture({ twoStages: true });
3995
+ let child;
3996
+ let childOutput = '';
3997
+ try {
3998
+ const row = await prepareIssueWorktree({
3999
+ baseRef: 'main',
4000
+ identity: 'synthetic-author',
4001
+ issueNumber: 17,
4002
+ repoRoot: root,
4003
+ });
4004
+ const bounds = {
4005
+ maxAggregateRssBytes: 1_000_000_000,
4006
+ minFreeDiskBytes: 1,
4007
+ maxNewOutputBytes: 2048,
4008
+ outputRoots: [output],
4009
+ };
4010
+ writeFileSync(failSecond, 'fail once');
4011
+ await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds }), /exit 7/u);
4012
+ assert.equal(readFileSync(counter, 'utf8'), '1');
4013
+ child = spawn(process.execPath, [
4014
+ '-e',
4015
+ `(async () => {
4016
+ const { withRuntimeSetupWriter } = await import(${JSON.stringify(new URL('./verification.js', import.meta.url).href)});
4017
+ await withRuntimeSetupWriter(${JSON.stringify(row.path)}, runner => runner.run([process.execPath, '-e', ${JSON.stringify(filesystemFixtureScript(output, 'cancel'))}], ${JSON.stringify(bounds)}));
4018
+ })().catch(error => { console.error(error.message); process.exitCode = 1; });`,
4019
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
4020
+ const capture = (chunk) => {
4021
+ childOutput += chunk.toString();
4022
+ if (childOutput.length > 1024 * 1024)
4023
+ child.kill('SIGKILL');
4024
+ };
4025
+ child.stdout.on('data', capture);
4026
+ child.stderr.on('data', capture);
4027
+ const closed = new Promise((resolve) => child.once('exit', () => resolve()));
4028
+ for (let attempt = 0; attempt < 500 && !existsSync(marker); attempt++)
4029
+ await new Promise((resolve) => setTimeout(resolve, 20));
4030
+ assert.ok(existsSync(marker), childOutput);
4031
+ const priorGate = JSON.parse(readFileSync(marker, 'utf8'));
4032
+ child.kill('SIGKILL');
4033
+ await closed;
4034
+ assert.equal(child.signalCode, 'SIGKILL');
4035
+ // Exercise the existing writer's stale-file recovery after the exact parent has exited.
4036
+ const writerPath = join(root, '.git', 'ai-delivery', 'writers@1', digestValue(row.path).slice(7) + '.json');
4037
+ const stale = new Date(Date.now() - 20_000);
4038
+ utimesSync(writerPath + '.lock', stale, stale);
4039
+ await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds }), /unsupported file/u);
4040
+ assert.equal(existsSync(priorGate.directory), false);
4041
+ assert.ok(existsSync(join(output, 'fifo')));
4042
+ assert.equal(readFileSync(counter, 'utf8'), '1');
4043
+ unlinkSync(join(output, 'fifo'));
4044
+ unlinkSync(failSecond);
4045
+ const resumed = await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds });
4046
+ assert.equal(resumed.stageReceipts.length, 2);
4047
+ assert.equal(readFileSync(counter, 'utf8'), '1');
4048
+ assert.equal(readFileSync(secondCounter, 'utf8'), '2');
4049
+ assert.ok(resumed.stageReceipts.every((receipt) => receipt.commands.every((command) => command.exitCode === 0)));
4050
+ const cached = await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds });
4051
+ assert.deepEqual(cached.stageReceipts.map((receipt) => receipt.receiptId), resumed.stageReceipts.map((receipt) => receipt.receiptId));
4052
+ await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: { ...bounds, maxNewOutputBytes: 4096 } });
4053
+ assert.equal(readFileSync(counter, 'utf8'), '2');
4054
+ }
4055
+ finally {
4056
+ child?.kill('SIGKILL');
4057
+ rmSync(root, { recursive: true, force: true });
4058
+ rmSync(output, { recursive: true, force: true });
4059
+ }
4060
+ });
3595
4061
  //# sourceMappingURL=lifecycle.test.js.map