@aviaratech/ai-delivery 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
- import { execFileSync, spawn } from 'node:child_process';
2
+ import { execFileSync, spawn, spawnSync } from 'node:child_process';
3
3
  import { createHash } from 'node:crypto';
4
- import { cpSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, readdirSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs';
4
+ import { cpSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, readdirSync, realpathSync, rmSync, symlinkSync, unlinkSync, utimesSync, writeFileSync, } from 'node:fs';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { dirname, join } from 'node:path';
7
7
  import { test, vi } from 'vitest';
@@ -16,12 +16,13 @@ import { digestValue } from './delivery/index.js';
16
16
  import { executeTool, resumedIssueUpdate, startTrackedIssue } from './dispatch.js';
17
17
  import { defaultBaseRef, gitCommonDir } from './git.js';
18
18
  import * as githubClient from './github/client.js';
19
+ import * as atomicJson from './utils/atomicJson.js';
19
20
  import { createIssue, developIssue, listIssueSubissues, readyCheck, resumeCreatedIssue, updateIssue, } from './issue.js';
20
21
  import { checkoutPr, finishIssue, listPrs, mergePr, prChecks, prInfo, publishPr, submitFormalReview } from './pr.js';
21
- import { updateIssueWorktreeDelivery } from './services/worktreeRegistry.js';
22
+ import { addWorktreeEntry, removeWorktreeEntry, updateIssueWorktreeDelivery } from './services/worktreeRegistry.js';
22
23
  import { assertDeliveryRuntimeAdmitted } from './services/deliveryAdmission.js';
23
24
  import { getDeliveryRecords } from './services/deliveryRecordService.js';
24
- import { createIssuePhaseEvidence, verifyIssue } from './verification.js';
25
+ import { createIssuePhaseEvidence, loadVerifiedRun, verifyIssue } from './verification.js';
25
26
  import { cleanupNonIssueWorktree, prepareIssueWorktree, preparePrWorktree, prepareStandaloneWorktree, } from './worktree.js';
26
27
  const personalRoute = vi.hoisted(() => ({
27
28
  active: false,
@@ -29,6 +30,10 @@ const personalRoute = vi.hoisted(() => ({
29
30
  headSha: '',
30
31
  reviews: [],
31
32
  submitted: 0,
33
+ created: false,
34
+ failCreate: false,
35
+ ready: false,
36
+ readyPromotions: 0,
32
37
  }));
33
38
  vi.mock('./github/client.js', async (importOriginal) => {
34
39
  const actual = await importOriginal();
@@ -43,7 +48,7 @@ vi.mock('./github/client.js', async (importOriginal) => {
43
48
  head: { sha: personalRoute.headSha, ref: 'issue/17' },
44
49
  base: { sha: personalRoute.baseSha, ref: 'main' },
45
50
  state: 'open',
46
- draft: true,
51
+ draft: !personalRoute.ready,
47
52
  user: { login: 'host-user' },
48
53
  });
49
54
  if (input.role === 'reviewer') {
@@ -101,6 +106,11 @@ vi.mock('./github/client.js', async (importOriginal) => {
101
106
  },
102
107
  };
103
108
  }
109
+ if (query.includes('DeliveryDiscovery') ||
110
+ query.includes('DeliveryRepository') ||
111
+ query.includes('ProjectDeliveryConfiguration')) {
112
+ throw Object.assign(new Error('Reviewer cannot discover author routing metadata'), { status: 403 });
113
+ }
104
114
  return syntheticDiscoveryClients().graphql(query, variables);
105
115
  },
106
116
  };
@@ -112,14 +122,29 @@ vi.mock('./github/client.js', async (importOriginal) => {
112
122
  });
113
123
  Object.assign(clients, {
114
124
  authenticatedAuthor: async () => ({ actorLogin: 'host-user', credentialIdentity: 'user:37' }),
115
- graphql: syntheticDiscoveryClients().graphql,
125
+ graphql: async (query, variables) => {
126
+ if (query.includes('markPullRequestReadyForReview')) {
127
+ personalRoute.ready = true;
128
+ personalRoute.readyPromotions += 1;
129
+ return { markPullRequestReadyForReview: { pullRequest: { id: 'PR-23', isDraft: false } } };
130
+ }
131
+ return syntheticDiscoveryClients().graphql(query, variables);
132
+ },
116
133
  rest: {
117
134
  issues: { get: async () => ({ data: { title: 'Synthetic issue' } }) },
118
135
  git: { getRef: async () => ({ data: { object: { sha: personalRoute.baseSha } } }) },
119
136
  pulls: {
120
- list: async () => ({ data: [] }),
121
- create: async () => ({ data: pr() }),
137
+ list: async () => ({ data: personalRoute.created ? [pr()] : [] }),
138
+ create: async () => {
139
+ if (personalRoute.failCreate) {
140
+ personalRoute.failCreate = false;
141
+ throw new Error('Synthetic response lost after branch push');
142
+ }
143
+ personalRoute.created = true;
144
+ return { data: pr() };
145
+ },
122
146
  get: async () => ({ data: pr() }),
147
+ listReviews: async () => ({ data: personalRoute.reviews }),
123
148
  },
124
149
  request: async () => Promise.reject(Object.assign(new Error('Rules unreadable'), { status: 403 })),
125
150
  },
@@ -194,6 +219,8 @@ async function fixture(options = {}) {
194
219
  git(root, 'remote', 'add', remoteName, join(root, '.git', 'remote.git'));
195
220
  writeFileSync(join(root, '.gitignore'), '.issue-cli/\n.worktrees/\n');
196
221
  writeFileSync(join(root, 'artifact.txt'), 'synthetic proof\n');
222
+ if (options.componentPolicy)
223
+ writeFileSync(join(root, 'component.txt'), 'second component\n');
197
224
  const config = {
198
225
  commandPolicy: {
199
226
  checks: { format: 'REQUIRED', gitClean: 'REQUIRED', lint: 'REQUIRED', test: 'REQUIRED', typecheck: 'REQUIRED' },
@@ -251,7 +278,7 @@ const bytes = value => 'sha256:' + createHash('sha256').update(value).digest('he
251
278
  const policyDigest = bytes(readFileSync(fileURLToPath(import.meta.url)));
252
279
  export const deliverySettings = ${JSON.stringify({ roles: config.roles, commandPolicy: config.commandPolicy })};
253
280
  export default {
254
- schemaVersion: 'RepositoryDeliveryPolicy@1',
281
+ schemaVersion: '${options.componentPolicy ? 'RepositoryDeliveryPolicy@2' : 'RepositoryDeliveryPolicy@1'}',
255
282
  classifyExactRange(input) {
256
283
  const content = { artifacts: [{ digest: bytes(readFileSync(new URL('./artifact.txt', import.meta.url))),
257
284
  path: 'artifact.txt', producer: 'synthetic' }],
@@ -261,9 +288,11 @@ export default {
261
288
  schemaVersion: 'ai-delivery.policy-evidence@1' };
262
289
  return { policyDigest, policyEvidence: { ...content, evidenceId: digest(content) },
263
290
  requiredStages: [{ id: 'check', dependsOn: [], semanticInputKeys: ['source'],
291
+ ${options.componentPolicy ? "semanticInputs: [{ key: 'source', digest: bytes(readFileSync(new URL('./artifact.txt', import.meta.url))) }]," : ''}
264
292
  resourceClass: 'source_only', commands: [{ label: 'count', argv: [process.execPath, '-e',
265
293
  ${JSON.stringify(options.firstStageScript ?? `const fs=require('fs');const p=${JSON.stringify(counter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));`)}] }] }${options.twoStages
266
294
  ? `, { id: 'second', dependsOn: ['check'], semanticInputKeys: ['source'],
295
+ ${options.componentPolicy ? "semanticInputs: [{ key: 'source', digest: bytes(readFileSync(new URL('./component.txt', import.meta.url))) }]," : ''}
267
296
  resourceClass: 'source_only', commands: [{ label: 'retry', argv: [process.execPath, '-e',
268
297
  ${JSON.stringify(options.secondStageScript ?? `const fs=require('fs');const p=${JSON.stringify(secondCounter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));if(fs.existsSync(${JSON.stringify(failSecond)}))process.exit(7);`)}] }] }`
269
298
  : ''}],
@@ -647,6 +676,443 @@ test('synthetic cutover restores exact pre-write state and resumes an incomplete
647
676
  rmSync(root, { recursive: true, force: true });
648
677
  }
649
678
  });
679
+ test('immediate successful commands establish ownership and remain reusable', async () => {
680
+ const { root } = await fixture();
681
+ try {
682
+ const row = await prepareIssueWorktree({
683
+ baseRef: 'main',
684
+ identity: 'synthetic-author',
685
+ issueNumber: 17,
686
+ repoRoot: root,
687
+ });
688
+ const policyPath = join(row.path, 'policy.mjs');
689
+ writeFileSync(policyPath, readFileSync(policyPath, 'utf8').replace("argv: [process.execPath, '-e',", "argv: ['/usr/bin/true',"));
690
+ git(row.path, 'add', 'policy.mjs');
691
+ git(row.path, 'commit', '-qm', 'immediate stage');
692
+ const first = await verifyIssue({ issueNumber: 17, repoRoot: row.path });
693
+ const reused = await verifyIssue({ issueNumber: 17, repoRoot: row.path });
694
+ assert.equal(first.stageReceipts[0].receiptId, reused.stageReceipts[0].receiptId);
695
+ const bounded = await verifyIssue({
696
+ issueNumber: 17,
697
+ repoRoot: row.path,
698
+ resourceBounds: { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 },
699
+ });
700
+ assert.equal(bounded.aggregate.result, 'passed');
701
+ assert.ok(bounded.resources.sampleCount > 0);
702
+ assert.deepEqual(readdirSync(join(root, '.git/ai-delivery/writers@1')), []);
703
+ }
704
+ finally {
705
+ rmSync(root, { recursive: true, force: true });
706
+ }
707
+ });
708
+ test('component verification recomposes current-source aggregates with only affected stage execution', async () => {
709
+ const { root, counter, secondCounter } = await fixture({ twoStages: true, componentPolicy: true });
710
+ try {
711
+ const row = await prepareIssueWorktree({
712
+ baseRef: 'main',
713
+ identity: 'synthetic-author',
714
+ issueNumber: 17,
715
+ repoRoot: root,
716
+ });
717
+ const resourceBounds = { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 };
718
+ const original = await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds });
719
+ writeFileSync(join(row.path, 'unrelated.txt'), 'documentation');
720
+ git(row.path, 'add', '.');
721
+ git(row.path, 'commit', '-qm', 'unrelated source');
722
+ const current = await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds });
723
+ assert.notEqual(current.classification.receiptId, original.classification.receiptId);
724
+ assert.deepEqual(current.stageReceipts.map((r) => r.receiptId), original.stageReceipts.map((r) => r.receiptId));
725
+ assert.equal(current.aggregate.classificationReceiptId, current.classification.receiptId);
726
+ assert.equal(readFileSync(counter, 'utf8'), '1');
727
+ assert.equal(readFileSync(secondCounter, 'utf8'), '1');
728
+ writeFileSync(join(row.path, 'component.txt'), 'changed second component');
729
+ git(row.path, 'add', '.');
730
+ git(row.path, 'commit', '-qm', 'second component');
731
+ const secondChanged = await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds });
732
+ assert.equal(secondChanged.stageReceipts[0].receiptId, original.stageReceipts[0].receiptId);
733
+ assert.equal(readFileSync(counter, 'utf8'), '1');
734
+ assert.equal(readFileSync(secondCounter, 'utf8'), '2');
735
+ writeFileSync(join(row.path, 'artifact.txt'), 'changed upstream component');
736
+ git(row.path, 'add', '.');
737
+ git(row.path, 'commit', '-qm', 'upstream component');
738
+ await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds });
739
+ assert.equal(readFileSync(counter, 'utf8'), '2');
740
+ assert.equal(readFileSync(secondCounter, 'utf8'), '3');
741
+ await createIssuePhaseEvidence({ issueNumber: 17, repoRoot: row.path, phase: 'verify' });
742
+ }
743
+ finally {
744
+ rmSync(root, { recursive: true, force: true });
745
+ }
746
+ });
747
+ test('quiet verification reports completion-based progress and remains cancellable', { timeout: 12_000 }, async () => {
748
+ const { root } = await fixture({ firstStageScript: 'setInterval(()=>{},1000);' });
749
+ const controller = new AbortController();
750
+ const progress = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
751
+ let running;
752
+ try {
753
+ const row = await prepareIssueWorktree({
754
+ baseRef: 'main',
755
+ identity: 'synthetic-author',
756
+ issueNumber: 17,
757
+ repoRoot: root,
758
+ });
759
+ running = verifyIssue({ issueNumber: 17, repoRoot: row.path, signal: controller.signal });
760
+ void running.catch(() => undefined);
761
+ let status;
762
+ for (let attempt = 0; attempt < 350; attempt++) {
763
+ const latest = progress.mock.calls
764
+ .map(([chunk]) => String(chunk))
765
+ .filter((line) => line.startsWith('ai-delivery.verify '))
766
+ .at(-1);
767
+ if (latest)
768
+ status = JSON.parse(latest.slice('ai-delivery.verify '.length));
769
+ if (Number(status?.elapsedMs) >= 5_000)
770
+ break;
771
+ await new Promise((resolve) => setTimeout(resolve, 20));
772
+ }
773
+ assert.equal(status?.completedCommands, 0);
774
+ assert.equal(status?.remainingCommands, 1);
775
+ assert.equal(status?.completedStages, 0);
776
+ assert.equal(status?.capturedOutputBytes, 0);
777
+ assert.ok(Number(status?.lastCompletedWorkAgeMs) >= 5_000);
778
+ controller.abort();
779
+ await assert.rejects(running, /cancelled/u);
780
+ }
781
+ finally {
782
+ controller.abort();
783
+ await running?.catch(() => undefined);
784
+ vi.restoreAllMocks();
785
+ rmSync(root, { recursive: true, force: true });
786
+ }
787
+ });
788
+ test('writer persistence failure retains recovery identity while cancellation cleans its owned command', async () => {
789
+ const directory = mkdtempSync(join(tmpdir(), 'ai-delivery-writer-failure-'));
790
+ const marker = join(directory, 'started');
791
+ const { root } = await fixture({
792
+ firstStageScript: `const fs=require('fs');fs.writeFileSync(${JSON.stringify(marker)},String(process.pid));setInterval(()=>{},1000);`,
793
+ });
794
+ const controller = new AbortController();
795
+ const original = atomicJson.writePrivateJsonFileAtomically;
796
+ let failWrites = false;
797
+ const persistence = vi.spyOn(atomicJson, 'writePrivateJsonFileAtomically').mockImplementation((path, value) => {
798
+ if (failWrites && path.includes('/writers@1/'))
799
+ throw new Error('ENOSPC: synthetic writer persistence failure');
800
+ original(path, value);
801
+ });
802
+ let running;
803
+ let owner;
804
+ try {
805
+ const row = await prepareIssueWorktree({
806
+ baseRef: 'main',
807
+ identity: 'synthetic-author',
808
+ issueNumber: 17,
809
+ repoRoot: root,
810
+ });
811
+ const writerPath = join(root, '.git/ai-delivery/writers@1', `${digestValue(row.path).slice(7)}.json`);
812
+ running = verifyIssue({ issueNumber: 17, repoRoot: row.path, signal: controller.signal }).catch((error) => error);
813
+ for (let attempt = 0; attempt < 100 && !existsSync(marker); attempt++)
814
+ await new Promise((resolve) => setTimeout(resolve, 20));
815
+ assert.ok(existsSync(marker));
816
+ const recorded = JSON.parse(readFileSync(writerPath, 'utf8'));
817
+ owner = recorded.command.root;
818
+ assert.equal(Number(readFileSync(marker, 'utf8')), owner.pid);
819
+ failWrites = true;
820
+ controller.abort();
821
+ const failure = String(await running);
822
+ assert.match(failure, /cancelled.*Writer state persistence failed.*ENOSPC/u);
823
+ assert.doesNotMatch(failure, /Owned process cleanup failed/u);
824
+ const remaining = spawnSync('ps', ['-p', String(owner.pid), '-o', 'stat='], { encoding: 'utf8' });
825
+ assert.ok(remaining.status === 1 || remaining.stdout.trim().startsWith('Z'));
826
+ assert.deepEqual(JSON.parse(readFileSync(writerPath, 'utf8')).command.root, owner);
827
+ assert.equal(existsSync(`${writerPath}.lock`), false);
828
+ failWrites = false;
829
+ await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path }), /live verification writer/u);
830
+ assert.equal(existsSync(writerPath), true);
831
+ }
832
+ finally {
833
+ controller.abort();
834
+ await running;
835
+ persistence.mockRestore();
836
+ if (owner !== undefined) {
837
+ const current = spawnSync('ps', ['-p', String(owner.pid), '-o', 'lstart='], { encoding: 'utf8' });
838
+ if (current.status === 0 && current.stdout.trim() === owner.identity)
839
+ process.kill(owner.pid, 'SIGKILL');
840
+ }
841
+ rmSync(root, { recursive: true, force: true });
842
+ rmSync(directory, { recursive: true, force: true });
843
+ }
844
+ });
845
+ test('verification excludes a second writer in the same worktree before command execution', async () => {
846
+ const marker = join(tmpdir(), `ai-delivery-writer-${String(process.pid)}`);
847
+ const { root } = await fixture({
848
+ firstStageScript: `const fs=require('fs');fs.appendFileSync(${JSON.stringify(marker)},'started\\n');setInterval(()=>{},1000);`,
849
+ });
850
+ const controller = new AbortController();
851
+ let running;
852
+ try {
853
+ const row = await prepareIssueWorktree({
854
+ baseRef: 'main',
855
+ identity: 'synthetic-author',
856
+ issueNumber: 17,
857
+ repoRoot: root,
858
+ });
859
+ running = verifyIssue({ issueNumber: 17, repoRoot: row.path, signal: controller.signal }).catch((error) => error);
860
+ for (let attempt = 0; attempt < 100 && !existsSync(marker); attempt++)
861
+ await new Promise((resolve) => setTimeout(resolve, 20));
862
+ assert.ok(existsSync(marker));
863
+ const contender = new AbortController();
864
+ const stopContender = setTimeout(() => contender.abort(), 500);
865
+ try {
866
+ await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, signal: contender.signal }), /writer|Lock acquisition/u);
867
+ }
868
+ finally {
869
+ clearTimeout(stopContender);
870
+ }
871
+ assert.equal(readFileSync(marker, 'utf8'), 'started\n');
872
+ controller.abort();
873
+ assert.match(String(await running), /cancelled/u);
874
+ }
875
+ finally {
876
+ controller.abort();
877
+ await running;
878
+ rmSync(root, { recursive: true, force: true });
879
+ rmSync(marker, { force: true });
880
+ }
881
+ });
882
+ test('live or unknown writer state fails closed even after a file lease becomes stale', async () => {
883
+ const { root, counter } = await fixture();
884
+ try {
885
+ const row = await prepareIssueWorktree({
886
+ baseRef: 'main',
887
+ identity: 'synthetic-author',
888
+ issueNumber: 17,
889
+ repoRoot: root,
890
+ });
891
+ const writerDirectory = join(root, '.git/ai-delivery/writers@1');
892
+ mkdirSync(writerDirectory, { recursive: true });
893
+ const writerPath = join(writerDirectory, `${digestValue(row.path).slice(7)}.json`);
894
+ const state = {
895
+ schemaVersion: 'ai-delivery.verification-writer@1',
896
+ writerId: '00000000-0000-4000-8000-000000000001',
897
+ worktreeDigest: digestValue(row.path),
898
+ owner: {
899
+ pid: process.pid,
900
+ identity: execFileSync('/bin/ps', ['-p', String(process.pid), '-o', 'lstart='], { encoding: 'utf8' }).trim(),
901
+ },
902
+ command: { phase: 'idle' },
903
+ };
904
+ writeFileSync(writerPath, JSON.stringify({ ...state, stateId: digestValue(state) }), { mode: 0o600 });
905
+ await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path }), /live verification writer/u);
906
+ const unknown = {
907
+ ...state,
908
+ owner: { ...state.owner, identity: 'replaced historical process' },
909
+ command: { phase: 'starting' },
910
+ };
911
+ writeFileSync(writerPath, JSON.stringify({ ...unknown, stateId: digestValue(unknown) }));
912
+ await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path }), /unknown command ownership/u);
913
+ assert.equal(existsSync(counter), false);
914
+ assert.equal(existsSync(writerPath), true);
915
+ }
916
+ finally {
917
+ rmSync(root, { recursive: true, force: true });
918
+ }
919
+ });
920
+ test('same-head worktrees overlap with isolated checkpoints and cancellation preserves the peer', { timeout: 15_000 }, async () => {
921
+ const { root } = await fixture({
922
+ firstStageScript: `const fs=require('fs');const cp=require('child_process');const path=require('path');const dir=cp.execFileSync('git',['rev-parse','--absolute-git-dir'],{encoding:'utf8'}).trim();const marker=path.join(dir,'peer-command');fs.writeFileSync(marker,'running');const wait=setInterval(()=>{if(fs.existsSync(marker+'.release')){clearInterval(wait);fs.writeFileSync(marker,'passed');}},20);`,
923
+ });
924
+ const cancel = new AbortController();
925
+ const parallelCancel = new AbortController();
926
+ let first;
927
+ let second;
928
+ let parallelRuns = [];
929
+ try {
930
+ const a = await prepareIssueWorktree({
931
+ baseRef: 'main',
932
+ identity: 'synthetic-author',
933
+ issueNumber: 17,
934
+ repoRoot: root,
935
+ });
936
+ const b = await prepareIssueWorktree({
937
+ baseRef: 'main',
938
+ identity: 'synthetic-author',
939
+ issueNumber: 18,
940
+ repoRoot: root,
941
+ });
942
+ assert.equal(git(a.path, 'rev-parse', 'HEAD'), git(b.path, 'rev-parse', 'HEAD'));
943
+ first = verifyIssue({ issueNumber: 17, repoRoot: a.path, signal: cancel.signal }).catch((error) => error);
944
+ second = verifyIssue({ issueNumber: 18, repoRoot: b.path });
945
+ void second.catch(() => undefined);
946
+ const markerA = join(git(a.path, 'rev-parse', '--absolute-git-dir'), 'peer-command');
947
+ const markerB = join(git(b.path, 'rev-parse', '--absolute-git-dir'), 'peer-command');
948
+ for (let attempt = 0; attempt < 100 && !(existsSync(markerA) && existsSync(markerB)); attempt++)
949
+ await new Promise((resolve) => setTimeout(resolve, 20));
950
+ assert.equal(readFileSync(markerA, 'utf8'), 'running');
951
+ assert.equal(readFileSync(markerB, 'utf8'), 'running');
952
+ cancel.abort();
953
+ assert.match(String(await first), /cancelled/u);
954
+ writeFileSync(`${markerB}.release`, 'ready');
955
+ const peer = await second;
956
+ writeFileSync(`${markerA}.release`, 'ready');
957
+ const retried = await verifyIssue({ issueNumber: 17, repoRoot: a.path });
958
+ assert.notEqual(peer.stageReceipts[0].input.inputId, retried.stageReceipts[0].input.inputId);
959
+ assert.equal(loadVerifiedRun(b.path, 18).manifestId, peer.manifestId);
960
+ assert.equal(loadVerifiedRun(a.path, 17).manifestId, retried.manifestId);
961
+ assert.equal(readFileSync(markerA, 'utf8'), 'passed');
962
+ assert.equal(readFileSync(markerB, 'utf8'), 'passed');
963
+ assert.deepEqual(peer.stageReceipts[0].commands, retried.stageReceipts[0].commands);
964
+ const resourceBounds = { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 };
965
+ const parallelStartedAt = Date.now();
966
+ for (const marker of [markerA, markerB]) {
967
+ rmSync(marker);
968
+ rmSync(`${marker}.release`);
969
+ }
970
+ parallelRuns = [
971
+ verifyIssue({ issueNumber: 17, repoRoot: a.path, resourceBounds, signal: parallelCancel.signal }),
972
+ verifyIssue({ issueNumber: 18, repoRoot: b.path, resourceBounds, signal: parallelCancel.signal }),
973
+ ];
974
+ const parallelRun = Promise.all(parallelRuns);
975
+ void parallelRun.catch(() => undefined);
976
+ for (let attempt = 0; attempt < 200 && !(existsSync(markerA) && existsSync(markerB)); attempt++)
977
+ await new Promise((resolve) => setTimeout(resolve, 20));
978
+ assert.equal(readFileSync(markerA, 'utf8'), 'running');
979
+ assert.equal(readFileSync(markerB, 'utf8'), 'running');
980
+ for (const marker of [markerA, markerB])
981
+ writeFileSync(`${marker}.release`, 'ready');
982
+ const parallel = await parallelRun;
983
+ const parallelElapsedMs = Date.now() - parallelStartedAt;
984
+ assert.deepEqual(parallel.map((run) => run.stageReceipts[0].commands), [peer.stageReceipts[0].commands, retried.stageReceipts[0].commands]);
985
+ assert.ok(parallel.every((run) => run.resources.sampleCount > 0));
986
+ const reused = await verifyIssue({ issueNumber: 18, repoRoot: b.path, resourceBounds });
987
+ assert.equal(reused.stageReceipts[0].receiptId, parallel[1].stageReceipts[0].receiptId);
988
+ const manifestA = join(root, '.git/ai-delivery/runs@2', digestValue(a.path).slice(7), `${peer.classification.head.sha}.json`);
989
+ const { manifestId: _foreignId, ...foreign } = parallel[1];
990
+ const foreignContent = { ...foreign, writer: { ...foreign.writer, worktreeDigest: digestValue(a.path) } };
991
+ writeFileSync(manifestA, JSON.stringify({ ...foreignContent, manifestId: digestValue(foreignContent) }));
992
+ assert.throws(() => loadVerifiedRun(a.path, 17), /environment|foreign/u);
993
+ writeFileSync(manifestA, JSON.stringify(parallel[0]));
994
+ console.log('WRITER_ISOLATION_RECEIPT', JSON.stringify({
995
+ sameHead: peer.classification.head.sha,
996
+ peerManifest: peer.manifestId,
997
+ retryManifest: retried.manifestId,
998
+ peerReused: true,
999
+ parallelElapsedMs,
1000
+ maxObservedRssBytes: Math.max(...parallel.map((run) => run.resources.maxSampledAggregateRssBytes)),
1001
+ }));
1002
+ }
1003
+ finally {
1004
+ cancel.abort();
1005
+ parallelCancel.abort();
1006
+ await first;
1007
+ await second?.catch(() => undefined);
1008
+ await Promise.allSettled(parallelRuns);
1009
+ rmSync(root, { recursive: true, force: true });
1010
+ }
1011
+ });
1012
+ test('interruption child verification', { skip: !process.env.AI_DELIVERY_INTERRUPTION_WORKTREE }, async () => {
1013
+ await verifyIssue({ issueNumber: 17, repoRoot: process.env.AI_DELIVERY_INTERRUPTION_WORKTREE });
1014
+ });
1015
+ test('interrupted writer recovers only its recorded command and resumes completed checkpoints', { timeout: 30_000 }, async () => {
1016
+ const { root, counter, secondCounter } = await fixture({
1017
+ twoStages: true,
1018
+ secondStageScript: `const fs=require('fs');const cp=require('child_process');const path=require('path');const dir=cp.execFileSync('git',['rev-parse','--git-common-dir'],{encoding:'utf8'}).trim();const count=path.join(dir,'second-stage-count.txt');fs.writeFileSync(count,String(Number(fs.existsSync(count)?fs.readFileSync(count,'utf8'):0)+1));if(!fs.existsSync(path.join(dir,'resume-interrupted')))setInterval(()=>{},1000);`,
1019
+ });
1020
+ let child;
1021
+ let workerPid;
1022
+ let ownedPid;
1023
+ let workerIdentity;
1024
+ let ownedIdentity;
1025
+ let peer;
1026
+ try {
1027
+ const row = await prepareIssueWorktree({
1028
+ baseRef: 'main',
1029
+ identity: 'synthetic-author',
1030
+ issueNumber: 17,
1031
+ repoRoot: root,
1032
+ });
1033
+ child = spawn(process.execPath, [
1034
+ join(process.cwd(), 'node_modules/vitest/vitest.mjs'),
1035
+ 'run',
1036
+ 'dist/lifecycle.test.js',
1037
+ '-t',
1038
+ '^interruption child verification$',
1039
+ ], {
1040
+ cwd: process.cwd(),
1041
+ stdio: 'ignore',
1042
+ env: { ...process.env, AI_DELIVERY_INTERRUPTION_WORKTREE: row.path },
1043
+ });
1044
+ const childClosed = new Promise((resolve) => child.once('close', resolve));
1045
+ const writerPath = join(root, '.git/ai-delivery/writers@1', `${digestValue(row.path).slice(7)}.json`);
1046
+ let recorded;
1047
+ for (let attempt = 0; attempt < 200; attempt++) {
1048
+ if (existsSync(writerPath))
1049
+ recorded = JSON.parse(readFileSync(writerPath, 'utf8'));
1050
+ if (recorded?.command.phase === 'running' && existsSync(secondCounter))
1051
+ break;
1052
+ await new Promise((resolve) => setTimeout(resolve, 20));
1053
+ }
1054
+ assert.equal(readFileSync(counter, 'utf8'), '1');
1055
+ assert.equal(readFileSync(secondCounter, 'utf8'), '1');
1056
+ assert.equal(recorded?.command.phase, 'running');
1057
+ workerPid = recorded.owner.pid;
1058
+ ownedPid = recorded.command.root.pid;
1059
+ workerIdentity = recorded.owner.identity;
1060
+ ownedIdentity = recorded.command.root.identity;
1061
+ const completedDir = join(root, '.git/ai-delivery/verification@1/stages/check');
1062
+ const completedCheckpoint = join(completedDir, readdirSync(completedDir)[0]);
1063
+ const completedDigest = sha256(completedCheckpoint);
1064
+ peer = spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'], { detached: true, stdio: 'ignore' });
1065
+ assert.equal(spawnSync('ps', ['-p', String(workerPid), '-o', 'lstart='], { encoding: 'utf8' }).stdout.trim(), workerIdentity);
1066
+ process.kill(workerPid, 'SIGKILL');
1067
+ workerPid = undefined;
1068
+ await childClosed;
1069
+ process.kill(ownedPid, 0);
1070
+ // A stale file lease is not permission to forget the durable command owner.
1071
+ const stale = new Date(Date.now() - 20_000);
1072
+ utimesSync(`${writerPath}.lock`, stale, stale);
1073
+ writeFileSync(join(root, '.git/resume-interrupted'), 'ready');
1074
+ const recovered = await verifyIssue({ issueNumber: 17, repoRoot: row.path });
1075
+ assert.equal(readFileSync(counter, 'utf8'), '1');
1076
+ assert.equal(readFileSync(secondCounter, 'utf8'), '2');
1077
+ assert.equal(sha256(completedCheckpoint), completedDigest);
1078
+ assert.equal(recovered.aggregate.result, 'passed');
1079
+ process.kill(peer.pid, 0);
1080
+ assert.equal(existsSync(writerPath), false);
1081
+ assert.equal(existsSync(`${writerPath}.lock`), false);
1082
+ const survivor = spawnSync('/bin/ps', ['-p', String(ownedPid), '-o', 'stat='], { encoding: 'utf8' });
1083
+ assert.ok((survivor.status === 1 && !survivor.stdout.trim()) ||
1084
+ (survivor.status === 0 && survivor.stdout.trim().startsWith('Z')));
1085
+ console.log('INTERRUPTION_RESUME_RECEIPT', JSON.stringify({
1086
+ completedCheckpoint: completedDigest,
1087
+ firstStageExecutions: 1,
1088
+ interruptedStageExecutions: 2,
1089
+ peerPreserved: true,
1090
+ aggregateId: recovered.aggregate.aggregateId,
1091
+ }));
1092
+ ownedPid = undefined;
1093
+ }
1094
+ finally {
1095
+ for (const [pid, identity] of [
1096
+ [workerPid, workerIdentity],
1097
+ [ownedPid, ownedIdentity],
1098
+ ])
1099
+ if (pid !== undefined && identity !== undefined) {
1100
+ const current = spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8' });
1101
+ if (current.status !== 0 || current.stdout.trim() !== identity)
1102
+ continue;
1103
+ try {
1104
+ process.kill(pid, 'SIGKILL');
1105
+ }
1106
+ catch (error) {
1107
+ assert.equal(error.code, 'ESRCH');
1108
+ }
1109
+ }
1110
+ for (const processHandle of [peer, child])
1111
+ if (processHandle?.exitCode === null && processHandle.signalCode === null)
1112
+ processHandle.kill('SIGKILL');
1113
+ rmSync(root, { recursive: true, force: true });
1114
+ }
1115
+ });
650
1116
  test('issue worktree verification resumes a completed stage and rejects changed inputs', async () => {
651
1117
  const { root, counter, runtimeEntryPath } = await fixture();
652
1118
  try {
@@ -1143,7 +1609,7 @@ test('a bounded passing run records measured resources in versioned evidence', a
1143
1609
  repoRoot: row.path,
1144
1610
  resourceBounds: { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 },
1145
1611
  });
1146
- assert.equal(run.schemaVersion, 'ai-delivery.run@2');
1612
+ assert.equal(run.schemaVersion, 'ai-delivery.run@3');
1147
1613
  const resources = run.resources;
1148
1614
  assert.ok(resources);
1149
1615
  assert.ok(resources.sampleCount > 0);
@@ -1738,25 +2204,46 @@ test('public PR dry run retains a configured host author and App reviewer', asyn
1738
2204
  rmSync(root, { recursive: true, force: true });
1739
2205
  }
1740
2206
  }, 15_000);
1741
- test('public dispatch publishes with selected host token and submits the configured App review once', async () => {
1742
- const { root, runtimeEntryPath } = await fixture({ personalAuthor: true });
2207
+ test.each([
2208
+ { transition: false, entry: 'source', highRisk: false, mcp: false },
2209
+ { transition: true, entry: 'primary', highRisk: false, mcp: false },
2210
+ { transition: true, entry: 'source', highRisk: false, mcp: false },
2211
+ { transition: true, entry: 'primary', highRisk: true, mcp: false },
2212
+ { transition: true, entry: 'source', highRisk: true, mcp: false },
2213
+ { transition: true, entry: 'primary', highRisk: true, mcp: true },
2214
+ ])('public dispatch publishes and reviews from $entry across policy transition $transition high risk $highRisk MCP $mcp', async ({ transition, entry, highRisk, mcp }) => {
2215
+ const { root, runtimeEntryPath } = await fixture({ personalAuthor: !transition });
1743
2216
  const originalPath = process.env.PATH;
2217
+ const driftEnv = 'AI_DELIVERY_TEST_CANDIDATE_CHECK';
2218
+ const previousDrift = process.env[driftEnv];
2219
+ delete process.env[driftEnv];
2220
+ let closeMcp;
1744
2221
  try {
2222
+ const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
2223
+ const admissionBefore = readFileSync(admissionPath, 'utf8');
2224
+ const primaryConfiguration = await loadDeliveryConfig(root);
1745
2225
  const row = await prepareIssueWorktree({
1746
2226
  baseRef: 'main',
1747
- identity: 'host-author',
2227
+ identity: transition ? 'synthetic-author' : 'host-author',
1748
2228
  issueNumber: 17,
1749
2229
  repoRoot: root,
1750
2230
  });
2231
+ if (transition) {
2232
+ const policyPath = join(row.path, 'policy.mjs');
2233
+ writeFileSync(policyPath, readFileSync(policyPath, 'utf8')
2234
+ .replace(JSON.stringify(primaryConfiguration.config.roles.author), JSON.stringify({
2235
+ authSource: 'personal',
2236
+ credentialEnv: { token: 'AUTHOR_TOKEN' },
2237
+ identity: 'host-author',
2238
+ }))
2239
+ .replace("risk: 'standard'", `risk: '${highRisk ? 'high' : 'standard'}'`) +
2240
+ `
2241
+ Object.defineProperty(deliverySettings.commandPolicy.checks, 'test', { get: () => process.env.${driftEnv} ?? 'REQUIRED' });
2242
+ `);
2243
+ }
1751
2244
  writeFileSync(join(row.path, 'change.txt'), 'host author publication\n');
1752
- git(row.path, 'add', 'change.txt');
2245
+ git(row.path, 'add', '.');
1753
2246
  git(row.path, 'commit', '-qm', 'synthetic host publication');
1754
- const run = await verifyIssue({ issueNumber: 17, repoRoot: row.path });
1755
- personalRoute.baseSha = run.classification.base.sha;
1756
- personalRoute.headSha = run.classification.head.sha;
1757
- personalRoute.reviews.length = 0;
1758
- personalRoute.submitted = 0;
1759
- personalRoute.active = true;
1760
2247
  const realGit = (originalPath ?? '')
1761
2248
  .split(':')
1762
2249
  .map((directory) => join(directory, 'git'))
@@ -1779,24 +2266,36 @@ exec "${realGit}" "$@"
1779
2266
  `);
1780
2267
  chmodSync(shim, 0o700);
1781
2268
  process.env.PATH = `${shimDir}:${originalPath ?? ''}`;
1782
- const execution = { repoRoot: row.path, runtimeEntryPath, identity: 'host-author' };
1783
- const published = (await executeTool('issue_pr_create', { issueNumber: 17 }, execution));
1784
- assert.equal(published.prNumber, 23);
1785
- assert.equal(existsSync(pushMarker), true);
1786
- assert.equal(published.reviewRoute.author.actorLogin, 'host-user');
1787
- assert.equal(published.reviewRoute.author.credentialSource, 'env:AUTHOR_TOKEN');
1788
- assert.equal(published.reviewRoute.reviewer.actorLogin, 'synthetic-reviewer[bot]');
1789
- const info = (await executeTool('issue_pr_info', { prNumber: 23 }, execution));
1790
- assert.equal(info.authorLogin, 'host-user');
1791
- const artifactContent = {
2269
+ const execution = { repoRoot: entry === 'primary' ? root : row.path, runtimeEntryPath, identity: 'host-author' };
2270
+ let call = (name, args) => executeTool(name, args, execution);
2271
+ if (mcp) {
2272
+ const server = createAiDeliveryMcpServer(execution);
2273
+ const client = new Client({ name: 'policy-transition-test', version: '1.0.0' });
2274
+ closeMcp = async () => {
2275
+ await client.close();
2276
+ await server.close();
2277
+ };
2278
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
2279
+ await server.connect(serverTransport);
2280
+ await client.connect(clientTransport);
2281
+ call = async (name, args) => {
2282
+ const result = await client.callTool({ name, arguments: args });
2283
+ const text = result.content.find((block) => block.type === 'text').text;
2284
+ if (result.isError)
2285
+ throw new Error(text);
2286
+ return JSON.parse(text);
2287
+ };
2288
+ }
2289
+ const classified = await verifyIssue({ issueNumber: 17, repoRoot: row.path });
2290
+ const reviewContent = {
1792
2291
  authorIdentity: 'host-author',
1793
2292
  checks: ['verified exact diff'],
1794
- diffScopeHash: digestValue(run.classification.changedPaths),
2293
+ diffScopeHash: digestValue(classified.classification.changedPaths),
1795
2294
  elapsedMs: 1,
1796
2295
  findings: [],
1797
- head: run.classification.head,
2296
+ head: classified.classification.head,
1798
2297
  issueNumber: 17,
1799
- prNumber: 23,
2298
+ prNumber: null,
1800
2299
  readOnly: true,
1801
2300
  requestedEffort: 'xhigh',
1802
2301
  requestedModel: 'gpt-6-astra',
@@ -1807,19 +2306,112 @@ exec "${realGit}" "$@"
1807
2306
  summary: 'Independent exact-head review',
1808
2307
  verdict: 'approve',
1809
2308
  };
2309
+ const prepublicationReview = join(root, '.git', 'prepublication-review.json');
2310
+ if (highRisk) {
2311
+ await assert.rejects(call('issue_verify', { issueNumber: 17 }), /independent exact-head approval/u);
2312
+ writeFileSync(prepublicationReview, JSON.stringify({ ...reviewContent, artifactId: digestValue(reviewContent) }));
2313
+ }
2314
+ const verified = (await call('issue_verify', {
2315
+ issueNumber: 17,
2316
+ ...(highRisk ? { prepublicationReview } : {}),
2317
+ }));
2318
+ const run = loadVerifiedRun(row.path, 17);
2319
+ const candidateConfiguration = await loadDeliveryConfig(row.path);
2320
+ assert.equal(run.classification.configDigest, candidateConfiguration.configDigest);
2321
+ assert.equal(run.classification.policyDigest, sha256(join(row.path, 'policy.mjs')));
2322
+ if (transition)
2323
+ assert.notEqual(run.classification.configDigest, primaryConfiguration.configDigest);
2324
+ assert.equal(readFileSync(admissionPath, 'utf8'), admissionBefore);
2325
+ const phaseEvidence = JSON.parse(readFileSync(join(root, '.git', 'ai-delivery', 'receipts', 'delivery@1', run.classification.head.sha, `${verified.evidenceId.slice(7)}.json`), 'utf8'));
2326
+ assert.equal(phaseEvidence.configDigest, candidateConfiguration.configDigest);
2327
+ assert.equal(phaseEvidence.aggregateId, run.aggregate.aggregateId);
2328
+ if (highRisk) {
2329
+ assert.equal(phaseEvidence.approval?.authorIdentity, 'host-author');
2330
+ assert.equal(phaseEvidence.approval?.reviewerIdentity, 'synthetic-reviewer');
2331
+ }
2332
+ personalRoute.baseSha = run.classification.base.sha;
2333
+ personalRoute.headSha = run.classification.head.sha;
2334
+ personalRoute.reviews.length = 0;
2335
+ personalRoute.submitted = 0;
2336
+ personalRoute.created = false;
2337
+ personalRoute.failCreate = false;
2338
+ personalRoute.ready = false;
2339
+ personalRoute.readyPromotions = 0;
2340
+ personalRoute.active = true;
2341
+ if (transition) {
2342
+ await assert.rejects(executeTool('issue_pr_create', { issueNumber: 17 }, { ...execution, identity: 'synthetic-author' }), /configured author identity/u);
2343
+ writeFileSync(join(row.path, 'dirty.txt'), 'unverified');
2344
+ await assert.rejects(executeTool('issue_pr_create', { issueNumber: 17 }, execution), /clean worktree/u);
2345
+ rmSync(join(row.path, 'dirty.txt'));
2346
+ git(row.path, 'commit', '--allow-empty', '-qm', 'unverified head');
2347
+ await assert.rejects(executeTool('issue_pr_create', { issueNumber: 17 }, execution));
2348
+ assert.equal(existsSync(pushMarker), false);
2349
+ git(row.path, 'reset', '--hard', run.classification.head.sha);
2350
+ personalRoute.baseSha = 'f'.repeat(40);
2351
+ await assert.rejects(call('issue_pr_create', { issueNumber: 17 }), /Remote base changed/u);
2352
+ assert.equal(existsSync(pushMarker), false);
2353
+ personalRoute.baseSha = run.classification.base.sha;
2354
+ personalRoute.failCreate = true;
2355
+ await assert.rejects(call('issue_pr_create', { issueNumber: 17 }), /response lost after branch push/u);
2356
+ assert.equal(existsSync(pushMarker), true);
2357
+ assert.equal(personalRoute.created, false);
2358
+ }
2359
+ const published = (await call('issue_pr_create', { issueNumber: 17 }));
2360
+ assert.equal(published.prNumber, 23);
2361
+ assert.equal(existsSync(pushMarker), true);
2362
+ assert.equal(published.reviewRoute.author.actorLogin, 'host-user');
2363
+ assert.equal(published.reviewRoute.author.credentialSource, 'env:AUTHOR_TOKEN');
2364
+ assert.equal(published.reviewRoute.reviewer.actorLogin, 'synthetic-reviewer[bot]');
2365
+ const info = (await call('issue_pr_info', { issueNumber: 17, prNumber: 23 }));
2366
+ assert.equal(info.authorLogin, 'host-user');
2367
+ assert.equal(readFileSync(admissionPath, 'utf8'), admissionBefore);
2368
+ const artifactContent = { ...reviewContent, prNumber: 23 };
1810
2369
  const artifact = JSON.stringify({ ...artifactContent, artifactId: digestValue(artifactContent) });
1811
2370
  const reviewInput = { issueNumber: 17, prNumber: 23, identity: 'synthetic-reviewer', artifact };
1812
- const first = (await executeTool('issue_pr_review', reviewInput, execution));
2371
+ if (transition) {
2372
+ personalRoute.headSha = 'b'.repeat(40);
2373
+ await assert.rejects(call('issue_pr_review', reviewInput), /head/u);
2374
+ assert.equal(personalRoute.submitted, 0);
2375
+ personalRoute.headSha = run.classification.head.sha;
2376
+ }
2377
+ if (transition && entry === 'source') {
2378
+ process.env[driftEnv] = 'SKIP';
2379
+ assert.notEqual((await loadDeliveryConfig(row.path)).configDigest, candidateConfiguration.configDigest);
2380
+ assert.equal((await loadDeliveryConfig(root)).configDigest, primaryConfiguration.configDigest);
2381
+ await assert.rejects(call('issue_pr_review', reviewInput), /classification is stale against config/u);
2382
+ assert.equal(personalRoute.submitted, 0);
2383
+ delete process.env[driftEnv];
2384
+ }
2385
+ const first = (await call('issue_pr_review', reviewInput));
1813
2386
  assert.equal(first.reviewState.status, 'still-required');
1814
2387
  assert.equal(first.reviewState.submittedReviewAuthorCanPushToRepository, false);
1815
2388
  assert.equal(personalRoute.submitted, 1);
1816
- const second = (await executeTool('issue_pr_review', reviewInput, execution));
2389
+ if (transition && entry === 'primary') {
2390
+ process.env[driftEnv] = 'SKIP';
2391
+ assert.notEqual((await loadDeliveryConfig(row.path)).configDigest, candidateConfiguration.configDigest);
2392
+ assert.equal((await loadDeliveryConfig(root)).configDigest, primaryConfiguration.configDigest);
2393
+ await assert.rejects(call('issue_pr_create', { issueNumber: 17, draft: false }), /classification is stale against config/u);
2394
+ assert.equal(personalRoute.readyPromotions, 0);
2395
+ delete process.env[driftEnv];
2396
+ await call('issue_pr_create', { issueNumber: 17, draft: false });
2397
+ assert.equal(personalRoute.readyPromotions, 1);
2398
+ }
2399
+ const second = (await call('issue_pr_review', reviewInput));
1817
2400
  assert.equal(second.receiptId, first.receiptId);
1818
2401
  assert.equal(personalRoute.submitted, 1);
1819
2402
  }
1820
2403
  finally {
2404
+ await closeMcp?.();
1821
2405
  personalRoute.active = false;
2406
+ personalRoute.created = false;
2407
+ personalRoute.failCreate = false;
1822
2408
  personalRoute.reviews.length = 0;
2409
+ personalRoute.ready = false;
2410
+ personalRoute.readyPromotions = 0;
2411
+ if (previousDrift === undefined)
2412
+ delete process.env[driftEnv];
2413
+ else
2414
+ process.env[driftEnv] = previousDrift;
1823
2415
  if (originalPath === undefined)
1824
2416
  delete process.env.PATH;
1825
2417
  else
@@ -1827,6 +2419,96 @@ exec "${realGit}" "$@"
1827
2419
  rmSync(root, { recursive: true, force: true });
1828
2420
  }
1829
2421
  }, 15_000);
2422
+ test.each(['runtime', 'primary-policy', 'witness', 'sibling', 'foreign-repository', 'missing-source'])('source-bound dispatch rejects invalid %s before running candidate stages', async (invalid) => {
2423
+ const { root, runtimeEntryPath, counter } = await fixture();
2424
+ let foreignRoot;
2425
+ try {
2426
+ const row = await prepareIssueWorktree({
2427
+ baseRef: 'main',
2428
+ identity: 'synthetic-author',
2429
+ issueNumber: 17,
2430
+ repoRoot: root,
2431
+ });
2432
+ let requestedRoot = root;
2433
+ const expected = {
2434
+ runtime: /source, capability or repository admission/u,
2435
+ 'primary-policy': /source, capability or repository admission/u,
2436
+ witness: /ownership witness/u,
2437
+ sibling: /primary or exact registered issue checkout/u,
2438
+ 'foreign-repository': /primary Git repository/u,
2439
+ 'missing-source': /git rev-parse failed/u,
2440
+ }[invalid];
2441
+ if (invalid === 'runtime')
2442
+ writeFileSync(join(dirname(runtimeEntryPath), 'changed.js'), 'unadmitted');
2443
+ if (invalid === 'primary-policy')
2444
+ writeFileSync(join(root, 'policy.mjs'), `${readFileSync(join(root, 'policy.mjs'), 'utf8')}\n// unadmitted controller policy\n`);
2445
+ if (invalid === 'witness')
2446
+ rmSync(join(root, '.git', 'ai-delivery', 'worktree-owners'), { recursive: true });
2447
+ if (invalid === 'sibling')
2448
+ requestedRoot = (await prepareStandaloneWorktree({
2449
+ name: 'sibling',
2450
+ branch: 'scratch/sibling',
2451
+ identity: 'synthetic-author',
2452
+ repoRoot: root,
2453
+ })).path;
2454
+ if (invalid === 'foreign-repository') {
2455
+ foreignRoot = (await fixture()).root;
2456
+ const foreign = await prepareIssueWorktree({
2457
+ baseRef: 'main',
2458
+ identity: 'synthetic-author',
2459
+ issueNumber: 17,
2460
+ repoRoot: foreignRoot,
2461
+ });
2462
+ await removeWorktreeEntry(row.path, root);
2463
+ await addWorktreeEntry(foreign, root);
2464
+ }
2465
+ if (invalid === 'missing-source')
2466
+ git(root, 'worktree', 'remove', row.path);
2467
+ await assert.rejects(executeTool('issue_verify', { issueNumber: 17 }, { repoRoot: requestedRoot, runtimeEntryPath }), expected);
2468
+ assert.equal(existsSync(counter), false);
2469
+ }
2470
+ finally {
2471
+ if (foreignRoot !== undefined)
2472
+ rmSync(foreignRoot, { recursive: true, force: true });
2473
+ rmSync(root, { recursive: true, force: true });
2474
+ }
2475
+ });
2476
+ test('source-bound publication preserves rejection of a legacy personal override for an App author', async () => {
2477
+ const { root, runtimeEntryPath } = await fixture();
2478
+ let restore;
2479
+ try {
2480
+ const row = await prepareIssueWorktree({
2481
+ baseRef: 'main',
2482
+ identity: 'synthetic-author',
2483
+ issueNumber: 17,
2484
+ repoRoot: root,
2485
+ });
2486
+ const run = await verifyIssue({ issueNumber: 17, repoRoot: row.path });
2487
+ const factory = githubClient.createDeliveryGitHubClients;
2488
+ const selected = vi.spyOn(githubClient, 'createDeliveryGitHubClients').mockImplementation(async (input) => {
2489
+ const clients = await factory(input);
2490
+ if (input.role !== 'author')
2491
+ return clients;
2492
+ Object.assign(clients.rest.issues, { get: async () => ({ data: { title: 'Synthetic issue' } }) });
2493
+ Object.assign(clients.rest.git, {
2494
+ getRef: async () => ({ data: { object: { sha: run.classification.base.sha } } }),
2495
+ });
2496
+ return { ...clients, authSource: 'personal', credentialSource: 'legacy:personal' };
2497
+ });
2498
+ restore = () => selected.mockRestore();
2499
+ await assert.rejects(executeTool('issue_pr_create', { issueNumber: 17 }, {
2500
+ repoRoot: root,
2501
+ runtimeEntryPath,
2502
+ identity: 'personal',
2503
+ personalAuth: true,
2504
+ }), /requires the configured author credential/u);
2505
+ assert.equal(git(join(root, '.git', 'remote.git'), 'for-each-ref', '--format=%(refname)', 'refs/heads/issue/17'), '');
2506
+ }
2507
+ finally {
2508
+ restore?.();
2509
+ rmSync(root, { recursive: true, force: true });
2510
+ }
2511
+ });
1830
2512
  test('scratch start derives safe names and reports the requested worktree', async () => {
1831
2513
  const { root, runtimeEntryPath } = await fixture();
1832
2514
  try {
@@ -1903,7 +2585,7 @@ test('MCP request rejects unsupported legacy options before lifecycle dispatch',
1903
2585
  }
1904
2586
  });
1905
2587
  async function syntheticLifecycle(routing) {
1906
- const { root } = await fixture(routing);
2588
+ const { root, runtimeEntryPath } = await fixture(routing);
1907
2589
  const remoteName = routing.remote;
1908
2590
  assert.equal(defaultBaseRef(root, remoteName), `refs/remotes/${remoteName}/main`);
1909
2591
  const remote = join(root, '.git', 'remote.git');
@@ -1943,7 +2625,7 @@ async function syntheticLifecycle(routing) {
1943
2625
  let reviewDecision = 'REVIEW_REQUIRED';
1944
2626
  const calls = [];
1945
2627
  const createPayloads = [];
1946
- const statuses = { Queued: 'todo', Active: 'active', Waiting: 'blocked', Shipped: 'done' };
2628
+ const statuses = { Queued: 'STATUS-0', Active: 'STATUS-1', Waiting: 'STATUS-2', Shipped: 'STATUS-3' };
1947
2629
  const statusName = (option) => Object.entries(statuses).find(([, id]) => id === option)?.[0];
1948
2630
  const pageInfo = { endCursor: null, hasNextPage: false };
1949
2631
  const issueFieldValues = (number = issueNumber) => [
@@ -2264,6 +2946,7 @@ async function syntheticLifecycle(routing) {
2264
2946
  }),
2265
2947
  },
2266
2948
  };
2949
+ let restoreDispatchClient;
2267
2950
  try {
2268
2951
  await assert.rejects(startTrackedIssue(context, { request: 'Unready task', develop: true }), /New issue is not ready/u);
2269
2952
  assert.equal(calls.filter((call) => call === 'issue:create').length, 0);
@@ -2443,7 +3126,19 @@ async function syntheticLifecycle(routing) {
2443
3126
  await assert.rejects(finishIssue(context, { issueNumber, prNumber, strategy: 'merge' }), /Delivery record store is invalid/u);
2444
3127
  assert.equal(existsSync(row.path), false);
2445
3128
  rmSync(blockedRecordPath, { recursive: true });
2446
- const finished = await finishIssue(context, { issueNumber, prNumber, strategy: 'merge' });
3129
+ const dispatchClient = vi.spyOn(githubClient, 'createDeliveryGitHubClients').mockImplementation(async () => ({
3130
+ ...context.clients,
3131
+ graphql: (async (query, variables) => query.includes('DeliveryDiscovery') ||
3132
+ query.includes('DeliveryRepository') ||
3133
+ query.includes('ProjectDeliveryConfiguration')
3134
+ ? syntheticDiscoveryClients().graphql(query, variables)
3135
+ : graphql(query, variables)),
3136
+ }));
3137
+ restoreDispatchClient = () => dispatchClient.mockRestore();
3138
+ const execution = { repoRoot: root, runtimeEntryPath, identity: 'synthetic-author' };
3139
+ const terminalMerge = (await executeTool('issue_pr_merge', { issueNumber, prNumber, strategy: 'merge' }, execution));
3140
+ assert.equal(terminalMerge.mergeSha, git(remote, 'rev-parse', 'refs/heads/main'));
3141
+ const finished = (await executeTool('issue_finish', { issueNumber, prNumber, strategy: 'merge' }, execution));
2447
3142
  assert.equal(finished.issueClosed, true);
2448
3143
  assert.equal(finished.cleaned, true);
2449
3144
  assert.equal(projectStatus, 'Shipped');
@@ -2452,7 +3147,9 @@ async function syntheticLifecycle(routing) {
2452
3147
  assert.equal(getDeliveryRecords(root).length, 1);
2453
3148
  assert.equal(getDeliveryRecords(root)[0]?.points, 4);
2454
3149
  assert.deepEqual(calls, ['issue:create', 'review:create', 'git:createCommit']);
2455
- assert.deepEqual(await finishIssue(context, { issueNumber, prNumber, strategy: 'merge' }), finished);
3150
+ assert.deepEqual(await executeTool('issue_finish', { issueNumber, prNumber, strategy: 'merge' }, execution), finished);
3151
+ await assert.rejects(executeTool('issue_finish', { issueNumber, prNumber: 24 }, execution), /exact terminal merge receipts/u);
3152
+ dispatchClient.mockRestore();
2456
3153
  assert.equal(getDeliveryRecords(root).length, 1);
2457
3154
  await assert.rejects(finishIssue(context, { issueNumber, prNumber: 24 }), /exact terminal merge receipts/u);
2458
3155
  const newlyStarted = await startTrackedIssue(context, { request: 'New synthetic task' });
@@ -2491,6 +3188,7 @@ async function syntheticLifecycle(routing) {
2491
3188
  assert.equal(calls.filter((call) => call === 'issue:create').length, beforeTracking + 1);
2492
3189
  }
2493
3190
  finally {
3191
+ restoreDispatchClient?.();
2494
3192
  rmSync(root, { recursive: true, force: true });
2495
3193
  }
2496
3194
  }