@aviaratech/ai-delivery 0.3.4 → 0.3.6

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 (55) hide show
  1. package/README.md +135 -20
  2. package/dist/agent.d.ts +2 -0
  3. package/dist/agent.js +1 -0
  4. package/dist/agent.js.map +1 -1
  5. package/dist/cli.js +77 -2
  6. package/dist/cli.js.map +1 -1
  7. package/dist/dispatch.js +39 -1
  8. package/dist/dispatch.js.map +1 -1
  9. package/dist/github/client.d.ts +1 -0
  10. package/dist/github/client.js +4 -1
  11. package/dist/github/client.js.map +1 -1
  12. package/dist/github/commandIdentityPolicy.js +3 -0
  13. package/dist/github/commandIdentityPolicy.js.map +1 -1
  14. package/dist/issue.d.ts +1 -0
  15. package/dist/issue.js +24 -10
  16. package/dist/issue.js.map +1 -1
  17. package/dist/lifecycle.test.js +496 -30
  18. package/dist/lifecycle.test.js.map +1 -1
  19. package/dist/mcp/index.js +1 -1
  20. package/dist/mcp/tools.d.ts +63 -0
  21. package/dist/mcp/tools.js +66 -0
  22. package/dist/mcp/tools.js.map +1 -1
  23. package/dist/pr.d.ts +4 -4
  24. package/dist/pr.js +10 -6
  25. package/dist/pr.js.map +1 -1
  26. package/dist/review.test.js +163 -0
  27. package/dist/review.test.js.map +1 -1
  28. package/dist/services/deliveryAdmission.d.ts +13 -2
  29. package/dist/services/deliveryAdmission.js +41 -2
  30. package/dist/services/deliveryAdmission.js.map +1 -1
  31. package/dist/services/worktreeRegistry.d.ts +12 -0
  32. package/dist/services/worktreeRegistry.js +146 -4
  33. package/dist/services/worktreeRegistry.js.map +1 -1
  34. package/dist/setup.d.ts +58 -0
  35. package/dist/setup.js +415 -0
  36. package/dist/setup.js.map +1 -0
  37. package/dist/setup.test.d.ts +1 -0
  38. package/dist/setup.test.js +744 -0
  39. package/dist/setup.test.js.map +1 -0
  40. package/dist/verification.d.ts +37 -0
  41. package/dist/verification.js +214 -15
  42. package/dist/verification.js.map +1 -1
  43. package/dist/worktree.d.ts +3 -0
  44. package/dist/worktree.js +38 -8
  45. package/dist/worktree.js.map +1 -1
  46. package/dist/worktreeTransition.d.ts +173 -0
  47. package/dist/worktreeTransition.js +1012 -0
  48. package/dist/worktreeTransition.js.map +1 -0
  49. package/dist/worktreeTransition.test.d.ts +1 -0
  50. package/dist/worktreeTransition.test.js +1179 -0
  51. package/dist/worktreeTransition.test.js.map +1 -0
  52. package/package.json +1 -1
  53. package/plugins/ai-delivery/.claude-plugin/plugin.json +1 -1
  54. package/plugins/ai-delivery/README.md +1 -1
  55. package/plugins/ai-delivery/skills/worktree-lifecycle/SKILL.md +9 -0
@@ -13,7 +13,7 @@ import { AI_DELIVERY_MCP_TOOLS } from './mcp/tools.js';
13
13
  import { createAiDeliveryMcpServer } from './mcp/index.js';
14
14
  import { loadDeliveryConfig, parseDeliveryConfig } from './config/deliveryConfig.js';
15
15
  import { digestValue } from './delivery/index.js';
16
- import { executeTool, resumedIssueUpdate, startTrackedIssue } from './dispatch.js';
16
+ import { contextFor, executeTool, resumedIssueUpdate, startTrackedIssue } from './dispatch.js';
17
17
  import { defaultBaseRef, gitCommonDir } from './git.js';
18
18
  import * as githubClient from './github/client.js';
19
19
  import * as atomicJson from './utils/atomicJson.js';
@@ -329,7 +329,7 @@ export default {
329
329
  mkdirSync(dirname(path), { recursive: true });
330
330
  }
331
331
  writeFileSync(runtimeEntryPath, 'synthetic CLI bytes\n');
332
- writeFileSync(packagePath, JSON.stringify({ name: '@aviaratech/ai-delivery', version: '0.1.0' }));
332
+ writeFileSync(packagePath, JSON.stringify({ name: '@aviaratech/ai-delivery', version: '0.1.0', bin: { 'ai-delivery': './dist/cli.js' } }));
333
333
  writeFileSync(mcpLauncherPath, 'synthetic MCP launcher bytes\n');
334
334
  writeFileSync(pluginManifestPath, JSON.stringify({ name: 'ai-delivery', packageVersion: '0.1.0', deliveryCapabilityVersion: 2 }));
335
335
  const content = {
@@ -351,7 +351,8 @@ export default {
351
351
  };
352
352
  const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
353
353
  mkdirSync(dirname(admissionPath), { recursive: true });
354
- writeFileSync(admissionPath, JSON.stringify({ ...content, admissionId: digestValue(content) }), { mode: 0o600 });
354
+ if (!options.omitRuntimeAdmission)
355
+ writeFileSync(admissionPath, JSON.stringify({ ...content, admissionId: digestValue(content) }), { mode: 0o600 });
355
356
  return { root, counter, secondCounter, failSecond, runtimeEntryPath };
356
357
  }
357
358
  catch (error) {
@@ -1879,6 +1880,37 @@ test('filesystem output from a sampled detached child crosses its declared root
1879
1880
  rmSync(root, { recursive: true, force: true });
1880
1881
  }
1881
1882
  });
1883
+ test('contained npm bin aliases are counted once and target growth still enforces the output bound', async () => {
1884
+ const outside = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-contained-alias-')));
1885
+ const target = join(outside, 'output', 'actual');
1886
+ const { root } = await fixture({
1887
+ firstStageScript: `require('fs').appendFileSync(${JSON.stringify(target)},'x'.repeat(512));`,
1888
+ });
1889
+ try {
1890
+ mkdirSync(dirname(target));
1891
+ writeFileSync(target, 'baseline');
1892
+ symlinkSync('./actual', join(dirname(target), 'alias'));
1893
+ const row = await prepareIssueWorktree({
1894
+ baseRef: 'main',
1895
+ identity: 'synthetic-author',
1896
+ issueNumber: 17,
1897
+ repoRoot: root,
1898
+ });
1899
+ const bounds = {
1900
+ maxAggregateRssBytes: 1_000_000_000,
1901
+ minFreeDiskBytes: 1,
1902
+ maxNewOutputBytes: 600,
1903
+ outputRoots: [dirname(target)],
1904
+ };
1905
+ const run = await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds });
1906
+ assert.equal(run.resources?.maxSampledNewOutputBytes, 512);
1907
+ await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: { ...bounds, maxNewOutputBytes: 100 } }), /filesystem output.*exceeded/u);
1908
+ }
1909
+ finally {
1910
+ rmSync(root, { recursive: true, force: true });
1911
+ rmSync(outside, { recursive: true, force: true });
1912
+ }
1913
+ });
1882
1914
  test('a symbolic link in declared output roots fails before publishing sampled output evidence', async () => {
1883
1915
  const { root } = await fixture();
1884
1916
  const outside = mkdtempSync(join(tmpdir(), 'ai-delivery-output-link-'));
@@ -2525,8 +2557,228 @@ test('scratch start derives safe names and reports the requested worktree', asyn
2525
2557
  rmSync(root, { recursive: true, force: true });
2526
2558
  }
2527
2559
  });
2560
+ test.each(['direct-develop', 'direct-start', 'develop', 'existing-start', 'new-start', 'scratch', 'standalone'])('development access preflight rejects absent reviewer credentials before %s preparation', async (entry) => {
2561
+ const { root, runtimeEntryPath } = await fixture();
2562
+ const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
2563
+ const admissionBefore = readFileSync(admissionPath, 'utf8');
2564
+ const syntheticClients = githubClient.createDeliveryGitHubClients;
2565
+ const actual = await vi.importActual('./github/client.js');
2566
+ const mocked = vi
2567
+ .spyOn(githubClient, 'createDeliveryGitHubClients')
2568
+ .mockImplementation((input) => input.role === 'reviewer' ? actual.createDeliveryGitHubClients({ ...input, env: {} }) : syntheticClients(input));
2569
+ const execution = { repoRoot: root, runtimeEntryPath, identity: 'synthetic-author' };
2570
+ const tracking = {
2571
+ title: 'Synthetic change',
2572
+ body: '## Outcome\nDeliver a verified change.\n\n## Scope\n- `artifact.txt`\n\n## Acceptance Criteria\n- [ ] Change is verified\n- [ ] Review is recorded\n\n## Verification\nRun `node --version`.',
2573
+ points: 2,
2574
+ develop: true,
2575
+ };
2576
+ try {
2577
+ const run = async () => {
2578
+ if (entry === 'direct-develop' || entry === 'direct-start') {
2579
+ const context = await contextFor(execution, 'develop');
2580
+ return entry === 'direct-develop' ? developIssue(context, 17) : startTrackedIssue(context, tracking);
2581
+ }
2582
+ if (entry === 'develop')
2583
+ return executeTool('issue_develop', { issueNumber: 17 }, execution);
2584
+ if (entry === 'existing-start')
2585
+ return executeTool('issue_start', { issueNumber: 17 }, execution);
2586
+ if (entry === 'new-start')
2587
+ return executeTool('issue_start', tracking, execution);
2588
+ if (entry === 'scratch')
2589
+ return executeTool('issue_start', { scratch: true, request: 'Inspect parser' }, execution);
2590
+ return executeTool('issue_worktree_create', { name: 'scratch-access', branch: 'scratch/access' }, execution);
2591
+ };
2592
+ await assert.rejects(run(), /Missing GitHub App credentials for reviewer role/u);
2593
+ assert.equal(existsSync(join(root, '.worktrees')), false);
2594
+ assert.equal(existsSync(join(root, '.issue-cli', 'worktrees.json')), false);
2595
+ assert.equal(readFileSync(admissionPath, 'utf8'), admissionBefore);
2596
+ }
2597
+ finally {
2598
+ mocked.mockRestore();
2599
+ rmSync(root, { recursive: true, force: true });
2600
+ }
2601
+ });
2602
+ test.each(['author-source', 'author-readback', 'same-actor', 'reviewer-repository'])('development access preflight rejects a mismatched %s route before issue reads or writes', async (failure) => {
2603
+ const { root, runtimeEntryPath } = await fixture({ personalAuthor: true });
2604
+ const syntheticClients = githubClient.createDeliveryGitHubClients;
2605
+ const mocked = vi.spyOn(githubClient, 'createDeliveryGitHubClients').mockImplementation(async (input) => {
2606
+ const clients = await syntheticClients(input);
2607
+ if (input.role === 'author' && failure === 'author-source')
2608
+ return { ...clients, authSource: 'app' };
2609
+ if (input.role === 'author' && failure === 'author-readback')
2610
+ return { ...clients, authenticatedAuthor: async () => ({ actorLogin: '', credentialIdentity: '' }) };
2611
+ if (input.role === 'reviewer' && failure === 'same-actor')
2612
+ return { ...clients, appActorLogin: async () => 'host-user' };
2613
+ if (input.role === 'reviewer' && failure === 'reviewer-repository')
2614
+ Object.assign(clients.rest.repos, { get: async () => ({ data: { full_name: 'example/other' } }) });
2615
+ return clients;
2616
+ });
2617
+ try {
2618
+ const context = await contextFor({ repoRoot: root, runtimeEntryPath, identity: 'host-author' }, 'develop');
2619
+ await assert.rejects(developIssue(context, 17), {
2620
+ 'author-source': /requires the configured author credential/u,
2621
+ 'author-readback': /Author GitHub identity readback is incomplete/u,
2622
+ 'same-actor': /same GitHub actor/u,
2623
+ 'reviewer-repository': /repository readback disagrees/u,
2624
+ }[failure]);
2625
+ assert.equal(existsSync(join(root, '.worktrees')), false);
2626
+ assert.equal(existsSync(join(root, '.git', 'ai-delivery', 'issue-info')), false);
2627
+ }
2628
+ finally {
2629
+ mocked.mockRestore();
2630
+ rmSync(root, { recursive: true, force: true });
2631
+ }
2632
+ });
2633
+ test('development access preflight permits the configured host author and independent App with unreadable rules', async () => {
2634
+ const { root, runtimeEntryPath } = await fixture({ personalAuthor: true });
2635
+ const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
2636
+ const admissionBefore = readFileSync(admissionPath, 'utf8');
2637
+ try {
2638
+ const row = (await executeTool('issue_worktree_create', { name: 'scratch-host', branch: 'scratch/host' }, { repoRoot: root, runtimeEntryPath, identity: 'host-author' }));
2639
+ assert.equal(row.identity, 'host-author');
2640
+ assert.equal(row.path, join(root, '.worktrees', 'scratch-host'));
2641
+ assert.equal(existsSync(row.path), true);
2642
+ assert.equal(readFileSync(admissionPath, 'utf8'), admissionBefore);
2643
+ }
2644
+ finally {
2645
+ rmSync(root, { recursive: true, force: true });
2646
+ }
2647
+ });
2648
+ test.each([
2649
+ 'direct-develop',
2650
+ 'direct-start',
2651
+ 'develop',
2652
+ 'existing-start',
2653
+ 'new-start',
2654
+ 'scratch',
2655
+ 'standalone',
2656
+ 'config-resolve',
2657
+ ])('development access preflight preserves the explicit personal override for %s', async (entry) => {
2658
+ const { root, runtimeEntryPath } = await fixture();
2659
+ const admissionPath = join(root, '.git', 'ai-delivery', 'runtime-admission.json');
2660
+ const admissionBefore = readFileSync(admissionPath, 'utf8');
2661
+ const factory = githubClient.createDeliveryGitHubClients;
2662
+ const actual = await vi.importActual('./github/client.js');
2663
+ const downstream = new Error('Reached the downstream issue API with the selected author');
2664
+ const selected = vi.spyOn(githubClient, 'createDeliveryGitHubClients').mockImplementation(async (input) => {
2665
+ const clients = await factory(input);
2666
+ if (input.role !== 'author' || input.personalAuth === undefined)
2667
+ return clients;
2668
+ const personal = await actual.createDeliveryGitHubClients({
2669
+ ...input,
2670
+ env: { GH_TOKEN: 'synthetic-development-token' },
2671
+ });
2672
+ Object.assign(clients.rest.issues, {
2673
+ get: async () => {
2674
+ throw downstream;
2675
+ },
2676
+ create: async () => {
2677
+ throw downstream;
2678
+ },
2679
+ });
2680
+ Object.assign(clients.rest, { request: async () => Promise.reject(new Error('Rules unavailable')) });
2681
+ return {
2682
+ ...personal,
2683
+ graphql: clients.graphql,
2684
+ rest: clients.rest,
2685
+ authenticatedAuthor: async () => ({ actorLogin: 'host-user', credentialIdentity: 'user:37' }),
2686
+ };
2687
+ });
2688
+ const execution = { repoRoot: root, runtimeEntryPath, identity: 'personal', personalAuth: true };
2689
+ const tracking = {
2690
+ title: 'Synthetic change',
2691
+ body: '## Outcome\nDeliver a verified change.\n\n## Scope\n- `artifact.txt`\n\n## Acceptance Criteria\n- [ ] Change is verified\n- [ ] Review is recorded\n\n## Verification\nRun `node --version`.',
2692
+ points: 2,
2693
+ develop: true,
2694
+ };
2695
+ try {
2696
+ if (entry === 'config-resolve') {
2697
+ // The prescribed CLI read must inspect this development selection without admission.
2698
+ rmSync(admissionPath);
2699
+ const originalArgv = process.argv;
2700
+ const originalExitCode = process.exitCode;
2701
+ let stdout = '';
2702
+ let stderr = '';
2703
+ let completed = () => { };
2704
+ const output = new Promise((resolve) => {
2705
+ completed = resolve;
2706
+ });
2707
+ const out = vi.spyOn(process.stdout, 'write').mockImplementation((value) => {
2708
+ stdout += String(value);
2709
+ completed();
2710
+ return true;
2711
+ });
2712
+ const err = vi.spyOn(process.stderr, 'write').mockImplementation((value) => {
2713
+ stderr += String(value);
2714
+ completed();
2715
+ return true;
2716
+ });
2717
+ try {
2718
+ process.argv = [
2719
+ process.execPath,
2720
+ 'ai-delivery',
2721
+ '--repo-root',
2722
+ root,
2723
+ '--identity',
2724
+ 'personal',
2725
+ '--personal-auth',
2726
+ 'config:resolve',
2727
+ ];
2728
+ await import('./cli.js');
2729
+ await output;
2730
+ assert.equal(stderr, '');
2731
+ const resolved = JSON.parse(stdout);
2732
+ assert.equal(resolved.reviewRoute.author.identity, 'personal');
2733
+ assert.equal(resolved.reviewRoute.author.authSource, 'personal');
2734
+ assert.equal(resolved.reviewRoute.author.actorLogin, 'host-user');
2735
+ assert.equal(resolved.reviewRoute.approvalEligibility, 'unknown');
2736
+ assert.equal(resolved.reviewRoute.rules.visibility, 'unknown');
2737
+ assert.equal(existsSync(admissionPath), false);
2738
+ assert.equal(existsSync(join(root, '.worktrees')), false);
2739
+ }
2740
+ finally {
2741
+ out.mockRestore();
2742
+ err.mockRestore();
2743
+ process.argv = originalArgv;
2744
+ process.exitCode = originalExitCode;
2745
+ }
2746
+ }
2747
+ else if (entry === 'scratch' || entry === 'standalone') {
2748
+ const row = (entry === 'scratch'
2749
+ ? await executeTool('issue_start', { scratch: true, request: 'Inspect parser' }, execution)
2750
+ : await executeTool('issue_worktree_create', { name: 'scratch-override', branch: 'scratch/override' }, execution));
2751
+ if (entry === 'standalone')
2752
+ assert.equal(row.identity, 'personal');
2753
+ assert.equal(existsSync(entry === 'scratch' ? row.worktreePath : row.path), true);
2754
+ }
2755
+ else {
2756
+ const run = async () => {
2757
+ if (entry === 'direct-develop' || entry === 'direct-start') {
2758
+ const context = await contextFor(execution, 'develop');
2759
+ return entry === 'direct-develop' ? developIssue(context, 17) : startTrackedIssue(context, tracking);
2760
+ }
2761
+ if (entry === 'develop')
2762
+ return executeTool('issue_develop', { issueNumber: 17 }, execution);
2763
+ if (entry === 'existing-start')
2764
+ return executeTool('issue_start', { issueNumber: 17 }, execution);
2765
+ return executeTool('issue_start', tracking, execution);
2766
+ };
2767
+ await assert.rejects(run(), (error) => error === downstream);
2768
+ assert.equal(existsSync(join(root, '.worktrees')), false);
2769
+ }
2770
+ if (entry !== 'config-resolve')
2771
+ assert.equal(readFileSync(admissionPath, 'utf8'), admissionBefore);
2772
+ }
2773
+ finally {
2774
+ selected.mockRestore();
2775
+ rmSync(root, { recursive: true, force: true });
2776
+ }
2777
+ });
2528
2778
  test('MCP exposes one implementation surface for native lifecycle commands', () => {
2529
2779
  assert.deepEqual(AI_DELIVERY_MCP_TOOLS.map((tool) => tool.name), [
2780
+ 'runtime_stage',
2781
+ 'runtime_admit',
2530
2782
  'issue_create',
2531
2783
  'issue_start',
2532
2784
  'issue_update',
@@ -2540,9 +2792,12 @@ test('MCP exposes one implementation surface for native lifecycle commands', ()
2540
2792
  'issue_pr_merge',
2541
2793
  'issue_finish',
2542
2794
  'issue_worktree_create',
2795
+ 'issue_worktree_transition_inspect',
2796
+ 'issue_worktree_transition_apply',
2543
2797
  ]);
2544
2798
  const validInputs = {
2545
2799
  issue_create: { title: 'Synthetic tracking parent' },
2800
+ issue_update: { issueNumber: 17, park: true },
2546
2801
  issue_ready_check: { issueNumber: 17 },
2547
2802
  issue_develop: { issueNumber: 17 },
2548
2803
  issue_verify: { issueNumber: 17 },
@@ -2550,7 +2805,17 @@ test('MCP exposes one implementation surface for native lifecycle commands', ()
2550
2805
  issue_pr_review: { issueNumber: 17, prNumber: 23, artifact: '{}' },
2551
2806
  issue_pr_merge: { issueNumber: 17, prNumber: 23, strategy: 'merge' },
2552
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
+ },
2553
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);
2554
2819
  for (const [name, input] of Object.entries(validInputs)) {
2555
2820
  const tool = AI_DELIVERY_MCP_TOOLS.find((candidate) => candidate.name === name);
2556
2821
  assert.ok(tool, `${name} has an MCP definition`);
@@ -2585,7 +2850,11 @@ test('MCP request rejects unsupported legacy options before lifecycle dispatch',
2585
2850
  }
2586
2851
  });
2587
2852
  async function syntheticLifecycle(routing) {
2588
- const { root, runtimeEntryPath } = await fixture(routing);
2853
+ const created = await fixture({ ...routing, omitRuntimeAdmission: routing.producerOnboarding === true });
2854
+ const root = created.root;
2855
+ let runtimeEntryPath = created.runtimeEntryPath;
2856
+ const originalPath = process.env.PATH;
2857
+ let restoreTransport;
2589
2858
  const remoteName = routing.remote;
2590
2859
  assert.equal(defaultBaseRef(root, remoteName), `refs/remotes/${remoteName}/main`);
2591
2860
  const remote = join(root, '.git', 'remote.git');
@@ -2614,6 +2883,9 @@ async function syntheticLifecycle(routing) {
2614
2883
  let trackingParent = false;
2615
2884
  let failFirstBlockerLink = true;
2616
2885
  let failNextProjectSync = false;
2886
+ let interruptNextStatusUpdate = false;
2887
+ let conflictNextStatusReadback = false;
2888
+ let projectStatusWrites = 0;
2617
2889
  let checksPassed = true;
2618
2890
  let prDraft = true;
2619
2891
  let prState = 'open';
@@ -2720,6 +2992,12 @@ async function syntheticLifecycle(routing) {
2720
2992
  nodes: projectStatus === null
2721
2993
  ? []
2722
2994
  : [
2995
+ {
2996
+ id: 'UNRELATED-ITEM',
2997
+ isArchived: false,
2998
+ content: { id: 'UNRELATED-ISSUE' },
2999
+ fieldValueByName: { name: 'Active', optionId: 'STATUS-1' },
3000
+ },
2723
3001
  {
2724
3002
  id: 'ITEM-17',
2725
3003
  isArchived: false,
@@ -2743,19 +3021,29 @@ async function syntheticLifecycle(routing) {
2743
3021
  return { addProjectV2ItemById: { item: { id: 'ITEM-17' } } };
2744
3022
  }
2745
3023
  if (query.includes('UpdateProjectDeliveryStatus')) {
3024
+ assert.equal(variables.projectId, 'PROJECT-1');
3025
+ assert.equal(variables.itemId, 'ITEM-17');
3026
+ projectStatusWrites += 1;
2746
3027
  projectStatus = statusName(String(variables.optionId)) ?? null;
3028
+ if (interruptNextStatusUpdate) {
3029
+ interruptNextStatusUpdate = false;
3030
+ throw new Error('synthetic ambiguous status write');
3031
+ }
2747
3032
  return { updateProjectV2ItemFieldValue: { projectV2Item: { id: 'ITEM-17' } } };
2748
3033
  }
2749
- if (query.includes('ProjectDeliveryItemReadback'))
3034
+ if (query.includes('ProjectDeliveryItemReadback')) {
3035
+ const contentId = conflictNextStatusReadback ? 'UNRELATED-ISSUE' : issue.node_id;
3036
+ conflictNextStatusReadback = false;
2750
3037
  return {
2751
3038
  node: {
2752
3039
  id: 'ITEM-17',
2753
3040
  isArchived: false,
2754
3041
  project: { id: 'PROJECT-1', number: 1 },
2755
- content: { id: issue.node_id },
3042
+ content: { id: contentId },
2756
3043
  fieldValueByName: { name: projectStatus, optionId: statuses[projectStatus] },
2757
3044
  },
2758
3045
  };
3046
+ }
2759
3047
  if (query.includes('blockedBy(first:'))
2760
3048
  return {
2761
3049
  repository: {
@@ -2776,6 +3064,10 @@ async function syntheticLifecycle(routing) {
2776
3064
  blockerOpen = true;
2777
3065
  return { addBlockedBy: { issue: { number: issueNumber } } };
2778
3066
  }
3067
+ if (query.includes('removeBlockedBy')) {
3068
+ blockerOpen = false;
3069
+ return { removeBlockedBy: { issue: { number: issueNumber } } };
3070
+ }
2779
3071
  if (query.includes('RepositoryId'))
2780
3072
  return { repository: { id: 'REPO-1' } };
2781
3073
  if (query.includes('UpdateExactRefs')) {
@@ -2948,6 +3240,65 @@ async function syntheticLifecycle(routing) {
2948
3240
  };
2949
3241
  let restoreDispatchClient;
2950
3242
  try {
3243
+ if (routing.producerOnboarding) {
3244
+ assert.equal(existsSync(join(root, 'AGENTS.md')), false);
3245
+ const archiveBase = join(root, '.git', 'producer-fixture');
3246
+ const packageSource = join(archiveBase, 'package');
3247
+ mkdirSync(archiveBase);
3248
+ cpSync(dirname(dirname(created.runtimeEntryPath)), packageSource, { recursive: true });
3249
+ cpSync(join(root, '.git/synthetic-runtime/plugin'), join(packageSource, 'plugins/ai-delivery'), {
3250
+ recursive: true,
3251
+ });
3252
+ const archive = join(archiveBase, 'reviewed.tgz');
3253
+ execFileSync('tar', ['-czf', archive, '-C', archiveBase, 'package']);
3254
+ const shimDir = join(archiveBase, 'bin');
3255
+ mkdirSync(shimDir);
3256
+ const npmShim = join(shimDir, 'npm');
3257
+ // Only installer and GitHub transports are synthetic; the public producer and validator are real.
3258
+ writeFileSync(npmShim, `#!${process.execPath}\nconst fs=require('fs'),path=require('path');const args=process.argv.slice(2);if(args[0]!=='install'||!['--omit=dev','--ignore-scripts','--no-audit','--no-fund'].every(x=>args.includes(x)))process.exit(91);const target=path.join(args[args.indexOf('--prefix')+1],'node_modules/@aviaratech/ai-delivery');fs.mkdirSync(path.dirname(target),{recursive:true});fs.cpSync(${JSON.stringify(packageSource)},target,{recursive:true});\n`);
3259
+ chmodSync(npmShim, 0o700);
3260
+ process.env.PATH = `${shimDir}:${originalPath ?? ''}`;
3261
+ const server = createAiDeliveryMcpServer({ repoRoot: root, identity: 'synthetic-author' });
3262
+ const client = new Client({ name: 'unrelated-public-onboarding', version: '1.0.0' });
3263
+ const [ct, st] = InMemoryTransport.createLinkedPair();
3264
+ await server.connect(st);
3265
+ await client.connect(ct);
3266
+ const setup = {
3267
+ expectedSourceCommit: baseSha,
3268
+ expectedConfigDigest: configuration.configDigest,
3269
+ runtimeDirectory: join(root, '.git', 'public-runtime-stage'),
3270
+ };
3271
+ try {
3272
+ const stageResponse = await client.callTool({
3273
+ name: 'runtime_stage',
3274
+ arguments: {
3275
+ ...setup,
3276
+ authority: 'runtime:stage',
3277
+ archivePath: archive,
3278
+ expectedArchiveSha256: sha256(archive),
3279
+ packageVersion: '0.1.0',
3280
+ },
3281
+ });
3282
+ assert.equal(stageResponse.isError, undefined);
3283
+ const stage = JSON.parse(stageResponse.content[0].text);
3284
+ const admitted = await client.callTool({
3285
+ name: 'runtime_admit',
3286
+ arguments: {
3287
+ ...setup,
3288
+ authority: 'runtime:admit',
3289
+ stageId: stage.stageId,
3290
+ expectedPriorAdmissionSha256: null,
3291
+ },
3292
+ });
3293
+ assert.equal(admitted.isError, undefined);
3294
+ runtimeEntryPath = stage.admission.cliPath;
3295
+ }
3296
+ finally {
3297
+ await client.close();
3298
+ await server.close();
3299
+ }
3300
+ await assertDeliveryRuntimeAdmitted({ repoRoot: root, runtimeEntryPath });
3301
+ }
2951
3302
  await assert.rejects(startTrackedIssue(context, { request: 'Unready task', develop: true }), /New issue is not ready/u);
2952
3303
  assert.equal(calls.filter((call) => call === 'issue:create').length, 0);
2953
3304
  const unsized = await createIssue(context, { title: 'Synthetic child', parentIssueNumber: 11 });
@@ -3013,36 +3364,107 @@ async function syntheticLifecycle(routing) {
3013
3364
  assert.equal(started.title, issue.title);
3014
3365
  const row = started;
3015
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');
3016
3402
  writeFileSync(join(row.path, 'change.txt'), 'feature\n');
3017
3403
  git(row.path, 'add', 'change.txt');
3018
3404
  git(row.path, 'commit', '-qm', 'synthetic change');
3019
3405
  headSha = git(row.path, 'rev-parse', 'HEAD');
3020
3406
  const run = await verifyIssue({ issueNumber, repoRoot: row.path });
3021
3407
  await assert.rejects(publishPr(context, { issueNumber, body: 'Closes #17' }), /Delivery Impact/);
3022
- await assert.rejects(publishPr(context, { issueNumber }), /Git push requires the selected author credential token/);
3023
- // The public push requires a real author App installation token. Bind a synthetic
3024
- // publication at that external boundary so the remaining public phases run offline.
3025
- git(row.path, 'push', '-q', remoteName, `${headSha}:refs/heads/issue/17`);
3026
- const publishEvidence = await createIssuePhaseEvidence({ issueNumber, phase: 'publish', repoRoot: row.path });
3027
- const publicationContent = {
3028
- baseSha,
3029
- evidenceId: publishEvidence.evidenceId,
3030
- headSha,
3031
- issueNumber,
3032
- prNumber,
3033
- schemaVersion: 'ai-delivery.publication@1',
3034
- };
3035
- const publicationPath = join(gitCommonDir(row.path), 'ai-delivery', 'publications', String(issueNumber), `${headSha}.json`);
3036
- mkdirSync(dirname(publicationPath), { recursive: true });
3037
- writeFileSync(publicationPath, JSON.stringify({ ...publicationContent, publicationId: digestValue(publicationContent) }), { mode: 0o600 });
3038
- await updateIssueWorktreeDelivery({
3039
- branch: row.branch,
3040
- issueNumber,
3041
- path: row.path,
3042
- prNumber,
3043
- projectRoot: root,
3044
- status: 'pr-published',
3045
- });
3408
+ if (routing.producerOnboarding) {
3409
+ const realGit = execFileSync('/usr/bin/which', ['git'], {
3410
+ encoding: 'utf8',
3411
+ env: { ...process.env, PATH: originalPath },
3412
+ }).trim();
3413
+ const shimDir = join(root, '.git/producer-fixture/bin');
3414
+ git(root, 'config', '--unset', `url.${remote}.insteadOf`);
3415
+ const gitShim = join(shimDir, 'git');
3416
+ writeFileSync(gitShim, `#!/bin/sh
3417
+ if [ "$1" = -c ] && [ "$2" = credential.helper= ]; then
3418
+ [ "$AI_DELIVERY_GIT_TOKEN" = synthetic-selected-author-token ] || exit 92
3419
+ shift 2
3420
+ operation="$1"; shift
3421
+ [ "$1" = https://github.com/example/widget.git ] || exit 93
3422
+ shift
3423
+ exec "${realGit}" -c credential.helper= "$operation" "${remote}" "$@"
3424
+ fi
3425
+ if [ "$1" = push ] && [ "$2" = -q ] && [ "$3" = "${remoteName}" ]; then
3426
+ shift 3
3427
+ exec "${realGit}" push -q "${remote}" "$@"
3428
+ fi
3429
+ exec "${realGit}" "$@"
3430
+ `);
3431
+ chmodSync(gitShim, 0o700);
3432
+ const transport = vi.spyOn(githubClient, 'withAuthorGitToken').mockImplementation(async (clients, callback) => {
3433
+ assert.equal(clients.role, 'author');
3434
+ return callback('synthetic-selected-author-token');
3435
+ });
3436
+ restoreTransport = () => transport.mockRestore();
3437
+ const published = await publishPr(context, { issueNumber });
3438
+ assert.equal(published.prNumber, prNumber);
3439
+ assert.equal(published.reviewRoute?.author.actorLogin, 'synthetic-author[bot]');
3440
+ assert.equal(published.reviewRoute?.approvalEligibility, 'unknown');
3441
+ }
3442
+ else {
3443
+ await assert.rejects(publishPr(context, { issueNumber }), /Git push requires the selected author credential token/);
3444
+ // The public push requires a real author App installation token. Bind a synthetic
3445
+ // publication at that external boundary so the remaining public phases run offline.
3446
+ git(row.path, 'push', '-q', remoteName, `${headSha}:refs/heads/issue/17`);
3447
+ const publishEvidence = await createIssuePhaseEvidence({ issueNumber, phase: 'publish', repoRoot: row.path });
3448
+ const publicationContent = {
3449
+ baseSha,
3450
+ evidenceId: publishEvidence.evidenceId,
3451
+ headSha,
3452
+ issueNumber,
3453
+ prNumber,
3454
+ schemaVersion: 'ai-delivery.publication@1',
3455
+ };
3456
+ const publicationPath = join(gitCommonDir(row.path), 'ai-delivery', 'publications', String(issueNumber), `${headSha}.json`);
3457
+ mkdirSync(dirname(publicationPath), { recursive: true });
3458
+ writeFileSync(publicationPath, JSON.stringify({ ...publicationContent, publicationId: digestValue(publicationContent) }), { mode: 0o600 });
3459
+ await updateIssueWorktreeDelivery({
3460
+ branch: row.branch,
3461
+ issueNumber,
3462
+ path: row.path,
3463
+ prNumber,
3464
+ projectRoot: root,
3465
+ status: 'pr-published',
3466
+ });
3467
+ }
3046
3468
  assert.equal((await listPrs(context)).pullRequests[0]?.number, prNumber);
3047
3469
  assert.equal((await prInfo(context, { prNumber })).authorLogin, 'synthetic-author[bot]');
3048
3470
  assert.equal((await prChecks(context, prNumber)).combinedStatus, 'success');
@@ -3188,6 +3610,8 @@ async function syntheticLifecycle(routing) {
3188
3610
  assert.equal(calls.filter((call) => call === 'issue:create').length, beforeTracking + 1);
3189
3611
  }
3190
3612
  finally {
3613
+ restoreTransport?.();
3614
+ process.env.PATH = originalPath;
3191
3615
  restoreDispatchClient?.();
3192
3616
  rmSync(root, { recursive: true, force: true });
3193
3617
  }
@@ -3197,4 +3621,46 @@ test.each([
3197
3621
  { remote: 'upstream', divergentOrigin: false },
3198
3622
  { remote: 'upstream', divergentOrigin: true },
3199
3623
  ])('synthetic lifecycle finishes with remote $remote and divergent origin $divergentOrigin', syntheticLifecycle, 15_000);
3624
+ test('an unrelated consumer uses public MCP setup through verified configured-author publication, counted review readback, merge and cleanup', { timeout: 0 }, async () => {
3625
+ await syntheticLifecycle({ remote: 'origin', divergentOrigin: false, producerOnboarding: true });
3626
+ });
3627
+ test.each(['escape', 'broken', 'cycle'])('output observation rejects %s aliases without running policy commands', async (kind) => {
3628
+ const { root, counter } = await fixture();
3629
+ const base = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-output-alias-')));
3630
+ try {
3631
+ const output = join(base, 'output');
3632
+ mkdirSync(output);
3633
+ if (kind === 'escape') {
3634
+ writeFileSync(join(base, 'outside'), 'outside');
3635
+ symlinkSync('../outside', join(output, 'alias'));
3636
+ }
3637
+ if (kind === 'broken')
3638
+ symlinkSync('./absent', join(output, 'alias'));
3639
+ if (kind === 'cycle') {
3640
+ symlinkSync('./other', join(output, 'alias'));
3641
+ symlinkSync('./alias', join(output, 'other'));
3642
+ }
3643
+ const row = await prepareIssueWorktree({
3644
+ baseRef: 'main',
3645
+ identity: 'synthetic-author',
3646
+ issueNumber: 17,
3647
+ repoRoot: root,
3648
+ });
3649
+ await assert.rejects(verifyIssue({
3650
+ issueNumber: 17,
3651
+ repoRoot: row.path,
3652
+ resourceBounds: {
3653
+ maxAggregateRssBytes: 1_000_000_000,
3654
+ minFreeDiskBytes: 1,
3655
+ maxNewOutputBytes: 1024,
3656
+ outputRoots: [output],
3657
+ },
3658
+ }), /escaping|broken|cyclic/u);
3659
+ assert.equal(existsSync(counter), false);
3660
+ }
3661
+ finally {
3662
+ rmSync(root, { recursive: true, force: true });
3663
+ rmSync(base, { recursive: true, force: true });
3664
+ }
3665
+ });
3200
3666
  //# sourceMappingURL=lifecycle.test.js.map