@aviaratech/ai-delivery 0.3.23 → 0.3.24

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 (112) hide show
  1. package/CONTRIBUTING.md +9 -0
  2. package/README.md +56 -447
  3. package/dist/agent.d.ts +5 -11
  4. package/dist/agent.js +4 -8
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli.js +12 -107
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config/deliveryConfig.d.ts +44 -55
  9. package/dist/config/deliveryConfig.js +85 -88
  10. package/dist/config/deliveryConfig.js.map +1 -1
  11. package/dist/config/deliveryConfig.test.js +13 -5
  12. package/dist/config/deliveryConfig.test.js.map +1 -1
  13. package/dist/delivery/common.js +7 -2
  14. package/dist/delivery/common.js.map +1 -1
  15. package/dist/delivery/delivery.test.js +2 -3
  16. package/dist/delivery/delivery.test.js.map +1 -1
  17. package/dist/delivery/index.d.ts +0 -6
  18. package/dist/delivery/index.js +0 -3
  19. package/dist/delivery/index.js.map +1 -1
  20. package/dist/delivery/legacy.d.ts +8 -0
  21. package/dist/delivery/legacy.js +5 -0
  22. package/dist/delivery/legacy.js.map +1 -0
  23. package/dist/delivery/policy.js +2 -13
  24. package/dist/delivery/policy.js.map +1 -1
  25. package/dist/directoryIndependent.test.d.ts +1 -0
  26. package/dist/directoryIndependent.test.js +262 -0
  27. package/dist/directoryIndependent.test.js.map +1 -0
  28. package/dist/dispatch.d.ts +29 -3
  29. package/dist/dispatch.js +160 -457
  30. package/dist/dispatch.js.map +1 -1
  31. package/dist/genericCompatibility.test.js +0 -35
  32. package/dist/genericCompatibility.test.js.map +1 -1
  33. package/dist/git.js +10 -3
  34. package/dist/git.js.map +1 -1
  35. package/dist/gitProcess.d.ts +6 -0
  36. package/dist/gitProcess.js +56 -0
  37. package/dist/gitProcess.js.map +1 -0
  38. package/dist/github/discovery.d.ts +1 -0
  39. package/dist/github/discovery.js +1 -1
  40. package/dist/github/discovery.js.map +1 -1
  41. package/dist/github/discovery.test.js +25 -20
  42. package/dist/github/discovery.test.js.map +1 -1
  43. package/dist/github/nativeIssueMetadata.d.ts +2 -2
  44. package/dist/github/nativeIssueMetadata.js.map +1 -1
  45. package/dist/github/projectDelivery.d.ts +1 -1
  46. package/dist/github/projectDelivery.js.map +1 -1
  47. package/dist/github/repo.d.ts +7 -1
  48. package/dist/github/repo.js +58 -2
  49. package/dist/github/repo.js.map +1 -1
  50. package/dist/issue.d.ts +11 -6
  51. package/dist/issue.js +136 -319
  52. package/dist/issue.js.map +1 -1
  53. package/dist/issueJournal.d.ts +1 -1
  54. package/dist/lifecycle.test.js +40 -7371
  55. package/dist/lifecycle.test.js.map +1 -1
  56. package/dist/logger.js +4 -4
  57. package/dist/logger.js.map +1 -1
  58. package/dist/mcp/tools.d.ts +26 -42
  59. package/dist/mcp/tools.js +33 -42
  60. package/dist/mcp/tools.js.map +1 -1
  61. package/dist/pluginInstaller.d.ts +1 -1
  62. package/dist/pluginPackaging.test.js +129 -0
  63. package/dist/pluginPackaging.test.js.map +1 -1
  64. package/dist/pr.d.ts +66 -114
  65. package/dist/pr.js +288 -1108
  66. package/dist/pr.js.map +1 -1
  67. package/dist/pr.test.js +295 -237
  68. package/dist/pr.test.js.map +1 -1
  69. package/dist/releaseReconciliation.test.d.ts +1 -0
  70. package/dist/releaseReconciliation.test.js +314 -0
  71. package/dist/releaseReconciliation.test.js.map +1 -0
  72. package/dist/review.d.ts +37 -7
  73. package/dist/review.js +106 -45
  74. package/dist/review.js.map +1 -1
  75. package/dist/review.test.js +30 -71
  76. package/dist/review.test.js.map +1 -1
  77. package/dist/services/agentReadinessService.d.ts +1 -1
  78. package/dist/services/agentReadinessService.js +2 -0
  79. package/dist/services/agentReadinessService.js.map +1 -1
  80. package/dist/services/deliveryAdmission.d.ts +4 -4
  81. package/dist/services/deliveryAdmission.js.map +1 -1
  82. package/dist/setup.test.js +10 -5
  83. package/dist/setup.test.js.map +1 -1
  84. package/dist/start.test.d.ts +1 -0
  85. package/dist/start.test.js +72 -0
  86. package/dist/start.test.js.map +1 -0
  87. package/dist/stdoutRegression.test.d.ts +1 -0
  88. package/dist/stdoutRegression.test.js +326 -0
  89. package/dist/stdoutRegression.test.js.map +1 -0
  90. package/dist/verification.d.ts +1 -27
  91. package/dist/verification.js +5 -1332
  92. package/dist/verification.js.map +1 -1
  93. package/dist/worktree.d.ts +1 -31
  94. package/dist/worktree.js +5 -200
  95. package/dist/worktree.js.map +1 -1
  96. package/dist/worktreeTransition.d.ts +1 -1
  97. package/dist/worktreeTransition.js +10 -5
  98. package/dist/worktreeTransition.js.map +1 -1
  99. package/dist/worktreeTransition.test.js +2 -3
  100. package/dist/worktreeTransition.test.js.map +1 -1
  101. package/package.json +1 -1
  102. package/plugins/ai-delivery/.claude-plugin/plugin.json +2 -2
  103. package/plugins/ai-delivery/README.md +3 -3
  104. package/plugins/ai-delivery/plugin.json +1 -1
  105. package/plugins/ai-delivery/runtime/dist/cli.js +50 -59
  106. package/plugins/ai-delivery/runtime/package.json +1 -1
  107. package/plugins/ai-delivery/skills/intake-create/SKILL.md +1 -1
  108. package/plugins/ai-delivery/skills/pr-handoff/SKILL.md +14 -21
  109. package/plugins/ai-delivery/skills/worktree-lifecycle/SKILL.md +12 -23
  110. package/dist/services/deliveryRecordService.d.ts +0 -47
  111. package/dist/services/deliveryRecordService.js +0 -199
  112. package/dist/services/deliveryRecordService.js.map +0 -1
package/dist/issue.js CHANGED
@@ -1,25 +1,13 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
1
  import { z } from 'zod';
4
2
  import { loadDeliveryConfig } from './config/deliveryConfig.js';
5
- import { assertPrivateFile, digestBytes, stableJson } from './delivery/common.js';
6
- import { digestValue } from './delivery/index.js';
3
+ import { digestValue } from './delivery/legacy.js';
7
4
  import { DeliveryError } from './errors.js';
8
5
  import { createDeliveryGitHubClients } from './github/client.js';
9
6
  import { NATIVE_ISSUE_API_VERSION, clearConfiguredNativeIssuePoints, getConfiguredNativeIssueMetadata, nativeIssueSettingsFromDeliveryConfig, setConfiguredNativeIssueMetadata, } from './github/nativeIssueMetadata.js';
10
7
  import { getIssueProjectStatus, projectSettingsFromDeliveryConfig, syncIssueProjectStatus, } from './github/projectDelivery.js';
11
8
  import { getNativeBlockerRelationships, hasNativeSubIssues, replaceBlockedBy, replaceParentIssue, resolveNativeRelationshipTargets, } from './github/relationships.js';
12
- import { resolveDeliveryRepo } from './github/repo.js';
13
- import { assertClean, git, gitCommonDir, gitExitCode, gitRoot, primaryGitRoot } from './git.js';
14
- import { mergePr, preflightReviewRoute, readMergedContinuationTerminal } from './pr.js';
9
+ import {} from './github/repo.js';
15
10
  import { evaluateAgentReadiness } from './services/agentReadinessService.js';
16
- import { assertDeliveryRuntimeAdmitted } from './services/deliveryAdmission.js';
17
- import { addWorktreeEntry, assertNativeIssueTrackingAdmission, getIssueWorktreeStrict, getWorktreeByIssue, listWorktreesStrict, committedContinuationPlanPath, CommittedContinuationPlanSchema, pendingCommittedContinuation, withCommittedContinuationRegistry, writeCommittedContinuationCheckpoint, } from './services/worktreeRegistry.js';
18
- import { withRuntimeSetupWriter, withWorktreeTransitionWriterAbsent } from './verification.js';
19
- import { authenticatedWorktreeActors, assertNativeWorktreeAcceptance } from './worktreeTransition.js';
20
- import { assertIssueWorktreeLocation, prepareIssueWorktree } from './worktree.js';
21
- import { ensurePrivateDirectoryDurably, writePrivateJsonFileAtomically } from './utils/atomicJson.js';
22
- import { withLock } from './utils/lockfile.js';
23
11
  import { JournalInputSchema, renderJournal } from './issueJournal.js';
24
12
  // Matches GitHub's first-party JavaScript comment-length validation:
25
13
  // https://github.com/github/gh-aw/blob/3ead8042c7b1edc2128ba1b28b1b80d00b7f4c22/actions/setup/js/comment_limit_helpers.cjs
@@ -42,47 +30,46 @@ export async function commentIssue(context, input, lifecycleKey) {
42
30
  const actor = (await context.clients.authenticatedAuthor()).actorLogin;
43
31
  if (!actor)
44
32
  throw new DeliveryError('Issue comment author identity is missing.');
45
- const directory = join(gitCommonDir(context.root), 'ai-delivery', 'journals');
46
- ensurePrivateDirectoryDurably(directory);
47
- return withLock(join(directory, String(input.issueNumber)), {
48
- operation: async () => {
49
- const issue = (await context.clients.rest.issues.get({ ...context.repo, issue_number: input.issueNumber })).data;
50
- if ('pull_request' in issue || issue.number !== input.issueNumber)
51
- throw new DeliveryError('Issue comment target is not the requested issue.');
52
- const matches = [];
53
- for (let page = 1;; page += 1) {
54
- const batch = (await context.clients.rest.issues.listComments({
55
- ...context.repo,
56
- issue_number: input.issueNumber,
57
- page,
58
- per_page: 100,
59
- })).data;
60
- matches.push(...batch.filter((comment) => (marker === undefined ? comment.body === body : comment.body?.endsWith(marker)) &&
61
- comment.user?.login.toLowerCase() === actor.toLowerCase()));
62
- if (batch.length < 100)
63
- break;
64
- }
65
- if (matches.length > 1)
66
- throw new DeliveryError('Issue comment has duplicate authored retry matches.');
67
- const existing = matches[0];
68
- const created = existing ??
69
- (await context.clients.rest.issues.createComment({ ...context.repo, issue_number: input.issueNumber, body }))
70
- .data;
71
- const readback = (await context.clients.rest.issues.getComment({ ...context.repo, comment_id: created.id })).data;
72
- const expectedIssuePath = `/repos/${context.repo.owner}/${context.repo.repo}/issues/${String(input.issueNumber)}`;
73
- if (!Number.isSafeInteger(readback.id) ||
74
- readback.id <= 0 ||
75
- readback.id !== created.id ||
76
- readback.html_url !== created.html_url ||
77
- readback.body !== (existing?.body ?? body) ||
78
- (lifecycleKey === undefined && readback.body !== body) ||
79
- readback.user?.login.toLowerCase() !== actor.toLowerCase() ||
80
- new URL(readback.issue_url).pathname.toLowerCase() !== expectedIssuePath.toLowerCase() ||
81
- !readback.html_url.startsWith('https://'))
82
- throw new DeliveryError('Issue comment readback disagrees with the authored issue comment.');
83
- return { commentId: readback.id, url: readback.html_url, body: readback.body, reused: existing !== undefined };
84
- },
85
- });
33
+ const operation = async () => {
34
+ const issue = (await context.clients.rest.issues.get({ ...context.repo, issue_number: input.issueNumber })).data;
35
+ if ('pull_request' in issue || issue.number !== input.issueNumber)
36
+ throw new DeliveryError('Issue comment target is not the requested issue.');
37
+ const matches = [];
38
+ for (let page = 1; page <= 20; page += 1) {
39
+ const batch = (await context.clients.rest.issues.listComments({
40
+ ...context.repo,
41
+ issue_number: input.issueNumber,
42
+ page,
43
+ per_page: 100,
44
+ })).data;
45
+ matches.push(...batch.filter((comment) => (marker === undefined ? comment.body === body : comment.body?.endsWith(marker)) &&
46
+ comment.user?.login.toLowerCase() === actor.toLowerCase()));
47
+ if (batch.length < 100)
48
+ break;
49
+ if (page === 20)
50
+ throw new DeliveryError('Issue comment history exceeds the bounded readback window.');
51
+ }
52
+ if (matches.length > 1)
53
+ throw new DeliveryError('Issue comment has duplicate authored retry matches.');
54
+ const existing = matches[0];
55
+ const created = existing ??
56
+ (await context.clients.rest.issues.createComment({ ...context.repo, issue_number: input.issueNumber, body }))
57
+ .data;
58
+ const readback = (await context.clients.rest.issues.getComment({ ...context.repo, comment_id: created.id })).data;
59
+ const expectedIssuePath = `/repos/${context.repo.owner}/${context.repo.repo}/issues/${String(input.issueNumber)}`;
60
+ if (!Number.isSafeInteger(readback.id) ||
61
+ readback.id <= 0 ||
62
+ readback.id !== created.id ||
63
+ readback.html_url !== created.html_url ||
64
+ readback.body !== (existing?.body ?? body) ||
65
+ (lifecycleKey === undefined && readback.body !== body) ||
66
+ readback.user?.login.toLowerCase() !== actor.toLowerCase() ||
67
+ new URL(readback.issue_url).pathname.toLowerCase() !== expectedIssuePath.toLowerCase() ||
68
+ !readback.html_url.startsWith('https://'))
69
+ throw new DeliveryError('Issue comment readback disagrees with the authored issue comment.');
70
+ return { commentId: readback.id, url: readback.html_url, body: readback.body, reused: existing !== undefined };
71
+ };
72
+ return operation();
86
73
  }
87
74
  export async function journalIssueStart(context, issueNumber) {
88
75
  const issue = (await context.clients.rest.issues.get({ ...context.repo, issue_number: issueNumber })).data;
@@ -99,13 +86,16 @@ export async function journalIssueStart(context, issueNumber) {
99
86
  }, `start:${issue.created_at}`);
100
87
  }
101
88
  export async function loadDeliveryContext(input) {
102
- const root = gitRoot(input.repoRoot);
103
- const loaded = await loadDeliveryConfig(root, {
89
+ const loaded = await loadDeliveryConfig(input.repoRoot, {
90
+ ...(input.repository === undefined ? {} : { repository: input.repository }),
104
91
  ...(input.personalAuth === undefined ? {} : { personalAuth: input.personalAuth }),
105
92
  ...(input.signal === undefined ? {} : { signal: input.signal }),
106
93
  });
107
94
  const config = loaded.config;
108
- const repo = resolveDeliveryRepo(config, root, loaded.remote);
95
+ const [owner, repositoryName] = config.repository.split('/');
96
+ if (!owner || !repositoryName)
97
+ throw new DeliveryError('Repository must be owner/name.');
98
+ const repo = { owner, repo: repositoryName };
109
99
  const clients = await createDeliveryGitHubClients({
110
100
  config,
111
101
  identity: input.identity,
@@ -125,14 +115,28 @@ export async function loadDeliveryContext(input) {
125
115
  settings: projectSettingsFromDeliveryConfig(config),
126
116
  writable: true,
127
117
  };
128
- return { clients, config, configuration: loaded, projectConfiguration, repo, root: primaryGitRoot(root) };
118
+ return { clients, config, configuration: loaded, projectConfiguration, repo, root: '' };
129
119
  }
130
- function labels(input) {
120
+ async function labels(context, input) {
131
121
  const selected = input ?? [];
132
- if (selected.some((label) => !/^(?:area|risk):[^\s:]+$/u.test(label)) || new Set(selected).size !== selected.length) {
133
- throw new DeliveryError('Issue labels must be unique area:* or risk:* taxonomy labels.');
122
+ if (selected.some((label) => !label.trim()) ||
123
+ new Set(selected.map((label) => label.toLowerCase())).size !== selected.length)
124
+ throw new DeliveryError('Issue labels must be non-empty and unique.');
125
+ if (selected.length === 0)
126
+ return [];
127
+ const available = new Set();
128
+ for (let page = 1; page <= 100; page++) {
129
+ const batch = (await context.clients.rest.issues.listLabelsForRepo({ ...context.repo, page, per_page: 100 })).data;
130
+ for (const label of batch)
131
+ available.add(label.name.toLowerCase());
132
+ if (batch.length < 100) {
133
+ const missing = selected.filter((label) => !available.has(label.toLowerCase()));
134
+ if (missing.length)
135
+ throw new DeliveryError(`Unknown repository label(s): ${missing.join(', ')}.`);
136
+ return [...selected];
137
+ }
134
138
  }
135
- return [...selected];
139
+ throw new DeliveryError('Repository label listing exceeds the bounded readback window.');
136
140
  }
137
141
  export class CreatedTrackingIssueError extends DeliveryError {
138
142
  created;
@@ -160,12 +164,9 @@ export async function createIssue(context, input) {
160
164
  repo,
161
165
  rest: clients.rest,
162
166
  });
163
- for (const related of new Set([...(input.blockedBy ?? []), input.parentIssueNumber].filter((number) => number !== undefined))) {
164
- assertNativeIssueTrackingAdmission(related, context.root);
165
- }
166
167
  const created = await clients.rest.issues.create({
167
168
  body: input.body ?? '',
168
- labels: labels(input.labels),
169
+ labels: await labels(context, input.labels),
169
170
  ...(input.milestone === undefined ? {} : { milestone: input.milestone }),
170
171
  owner: repo.owner,
171
172
  repo: repo.repo,
@@ -493,9 +494,8 @@ export async function updateIssue(context, input) {
493
494
  if (input.supersededBy !== undefined &&
494
495
  (!Number.isSafeInteger(input.supersededBy) || input.supersededBy <= 0 || input.supersededBy === input.issueNumber))
495
496
  throw new DeliveryError('A superseding issue must be a different positive issue number.');
496
- assertNativeIssueTrackingAdmission(input.issueNumber, context.root);
497
497
  const { clients, config, repo } = context;
498
- const acceptedLabels = input.labels === undefined ? undefined : labels(input.labels);
498
+ const acceptedLabels = input.labels === undefined ? undefined : await labels(context, input.labels);
499
499
  if (input.issueType !== undefined && !config.native.issueTypes.includes(input.issueType)) {
500
500
  throw new DeliveryError('Unsupported configured Issue Type.');
501
501
  }
@@ -685,34 +685,6 @@ export async function updateIssue(context, input) {
685
685
  export async function resumeCreatedIssue(context, input) {
686
686
  return updateIssue(context, input);
687
687
  }
688
- function issueSnapshotPath(repoRoot, issueNumber) {
689
- return join(gitCommonDir(repoRoot), 'ai-delivery', 'issue-info', `${issueNumber}.json`);
690
- }
691
- export function cachedIssueInfo(repoRoot, issueNumber) {
692
- if (!Number.isSafeInteger(issueNumber) || issueNumber <= 0)
693
- throw new DeliveryError('Issue number must be positive.');
694
- let snapshot;
695
- try {
696
- snapshot = JSON.parse(readFileSync(issueSnapshotPath(repoRoot, issueNumber), 'utf8'));
697
- }
698
- catch {
699
- throw new DeliveryError('No readable cached issue snapshot exists.');
700
- }
701
- if (typeof snapshot !== 'object' || snapshot === null || Array.isArray(snapshot)) {
702
- throw new DeliveryError('Cached issue snapshot is invalid.');
703
- }
704
- const { snapshotId, ...content } = snapshot;
705
- if (content.schemaVersion !== 'ai-delivery.issue-info@1' ||
706
- typeof content.savedAt !== 'string' ||
707
- typeof snapshotId !== 'string' ||
708
- snapshotId !== digestValue(content) ||
709
- typeof content.issue !== 'object' ||
710
- content.issue === null ||
711
- content.issue.issueNumber !== issueNumber) {
712
- throw new DeliveryError('Cached issue snapshot is invalid or foreign.');
713
- }
714
- return content.issue;
715
- }
716
688
  export async function issueInfo(context, issueNumber) {
717
689
  const { clients, config, repo } = context;
718
690
  const issue = (await clients.rest.issues.get({ issue_number: issueNumber, owner: repo.owner, repo: repo.repo })).data;
@@ -748,17 +720,8 @@ export async function issueInfo(context, issueNumber) {
748
720
  state: issue.state,
749
721
  title: issue.title,
750
722
  url: issue.html_url,
751
- worktree: getWorktreeByIssue(issueNumber, context.root)?.path ?? null,
752
- };
753
- const content = {
754
- issue: info,
755
- savedAt: new Date().toISOString(),
756
- schemaVersion: 'ai-delivery.issue-info@1',
723
+ worktree: null,
757
724
  };
758
- writePrivateJsonFileAtomically(issueSnapshotPath(context.root, issueNumber), {
759
- ...content,
760
- snapshotId: digestValue(content),
761
- });
762
725
  return info;
763
726
  }
764
727
  export async function listIssueSubissues(context, issueNumber) {
@@ -779,224 +742,78 @@ export async function readyCheck(context, issueNumber) {
779
742
  blockedBy: info.blockedBy,
780
743
  body: info.body,
781
744
  ...(info.points === undefined ? {} : { points: info.points }),
782
- repoRoot: context.root,
745
+ repoRoot: undefined,
783
746
  title: info.title,
784
747
  trackingParent,
785
748
  });
786
749
  }
787
- function continuationOperatorBody(plan) {
788
- return stableJson({
789
- schemaVersion: 'ai-delivery.worktree-continuation-authority@1',
790
- plan,
791
- acceptedScope: 'the-complete-content-addressed-committed-descendant-plan-and-every-original',
792
- operationalClosure: 'all-other-launchers-and-writers-are-quiescent-no-unknown-writers',
793
- maintainedExclusion: 'all-other-launchers-and-writers-remain-excluded-through-apply-and-exact-plan-replay',
794
- historicalAuthority: 'preservation-only-no-current-verification-review-retirement-or-hold-release',
795
- });
796
- }
797
- function continuationReviewerBody(plan, authorityCommentId) {
798
- return stableJson({
799
- schemaVersion: 'ai-delivery.worktree-continuation-acceptance@1',
800
- plan,
801
- authorityCommentId,
802
- authorityDigest: digestBytes(Buffer.from(continuationOperatorBody(plan))),
803
- acceptedScope: 'independent-acceptance-of-the-complete-plan-and-maintained-writer-exclusion',
804
- result: 'approved',
805
- });
806
- }
807
- async function continueCommittedDescendant(context, issueNumber, selected, runtimeEntryPath) {
808
- if (context.config.roles.author.authSource !== 'personal')
809
- throw new DeliveryError('Committed continuation requires configured personal author authentication.');
810
- await withWorktreeTransitionWriterAbsent(context.root, selected.path, () => withCommittedContinuationRegistry(context.root, issueNumber, async (registry) => {
811
- const original = registry.pending?.plan.row ?? registry.current;
812
- assertIssueWorktreeLocation(original, context.root);
813
- assertClean(original.path);
814
- const actors = await authenticatedWorktreeActors(context);
815
- const historical = await readMergedContinuationTerminal(context, original, actors.operator);
816
- const head = {
817
- sha: git(original.path, 'rev-parse', 'HEAD'),
818
- tree: git(original.path, 'rev-parse', 'HEAD^{tree}'),
819
- };
820
- if (head.sha === historical.terminal.headSha ||
821
- gitExitCode(original.path, 'merge-base', '--is-ancestor', historical.terminal.headSha, head.sha) !== 0)
822
- throw new DeliveryError('Committed continuation requires a strict committed descendant of its exact terminal head.');
823
- if (!context.configuration)
824
- throw new DeliveryError('Committed continuation requires discovered current configuration.');
825
- const readRuntime = () => assertDeliveryRuntimeAdmitted({
826
- repoRoot: context.root,
827
- configuration: context.configuration,
828
- ...(runtimeEntryPath === undefined ? {} : { runtimeEntryPath }),
829
- });
830
- const currentRuntime = await readRuntime();
831
- const path = committedContinuationPlanPath(context.root, issueNumber, head.sha);
832
- const stored = registry.pending?.plan ??
833
- (existsSync(path)
834
- ? CommittedContinuationPlanSchema.parse(JSON.parse(assertPrivateFile(path).toString('utf8')).plan)
835
- : undefined);
836
- const { prNumber: _priorPr, ...retained } = original;
837
- const content = {
838
- schemaVersion: 'ai-delivery.committed-continuation-plan@1',
839
- repository: context.config.repository,
840
- repoRoot: context.root,
841
- configDigest: context.configuration.configDigest,
842
- currentRuntime,
843
- row: original,
844
- replacement: {
845
- ...retained,
846
- type: 'issue',
847
- issueNumber,
848
- identity: context.config.roles.author.identity,
849
- status: 'active',
850
- updatedAt: stored?.replacement.updatedAt ?? new Date().toISOString(),
851
- },
852
- head,
853
- terminalHead: { sha: historical.terminal.headSha, tree: historical.terminal.headTree },
854
- terminalMergeId: historical.terminal.mergeId,
855
- operator: actors.operator,
856
- reviewerActor: actors.reviewerActor,
857
- preserved: [...historical.preserved, registry.ownerWitness],
858
- };
859
- const plan = CommittedContinuationPlanSchema.parse({ ...content, planId: digestValue(content) });
860
- if (stored && stableJson(stored) !== stableJson(plan))
861
- throw new DeliveryError('Committed continuation approved source, configuration or custody plan drifted.');
862
- registry.prepare();
863
- writeCommittedContinuationCheckpoint(path, {
864
- plan,
865
- operatorBody: continuationOperatorBody(plan),
866
- reviewerBody: continuationReviewerBody(plan, 0),
867
- });
868
- let ids = registry.pending && {
869
- authorityCommentId: registry.pending.authorityCommentId,
870
- acceptanceCommentId: registry.pending.acceptanceCommentId,
871
- };
872
- if (!ids) {
873
- const comments = [];
874
- for (let page = 1;; page += 1) {
875
- const batch = (await context.clients.rest.issues.listComments({
876
- ...context.repo,
877
- issue_number: issueNumber,
878
- per_page: 100,
879
- page,
880
- })).data;
881
- comments.push(...batch);
882
- if (batch.length < 100)
883
- break;
884
- }
885
- const authorities = comments.filter((comment) => comment.body === continuationOperatorBody(plan) &&
886
- comment.user?.type === 'User' &&
887
- comment.user.login.toLowerCase() === actors.operator.actorLogin.toLowerCase() &&
888
- `user:${String(comment.user.id)}` === actors.operator.credentialIdentity);
889
- const authority = authorities.length === 1 ? authorities[0] : undefined;
890
- const acceptances = authority
891
- ? comments.filter((comment) => comment.body === continuationReviewerBody(plan, authority.id) &&
892
- comment.user?.type === 'Bot' &&
893
- comment.user.login.toLowerCase() === actors.reviewerActor.toLowerCase())
894
- : [];
895
- if (!authority || acceptances.length !== 1 || !acceptances[0])
896
- throw new DeliveryError(`Committed continuation requires exact native operator and configured reviewer-App acceptance. Saved plan: ${path}`);
897
- ids = { authorityCommentId: authority.id, acceptanceCommentId: acceptances[0].id };
750
+ async function linkedIssueBranches(context, issueNumber) {
751
+ const branches = [];
752
+ const cursors = new Set();
753
+ let cursor = null;
754
+ for (let page = 0; page < 20; page++) {
755
+ const response = await context.clients.graphql('query DeliveryIssueBranches($owner:String!,$repo:String!,$issue:Int!,$cursor:String){repository(owner:$owner,name:$repo){issue(number:$issue){linkedBranches(first:100,after:$cursor){nodes{ref{name target{oid} repository{nameWithOwner}}} pageInfo{hasNextPage endCursor}}}}}', { owner: context.repo.owner, repo: context.repo.repo, issue: issueNumber, cursor });
756
+ const connection = response.repository?.issue?.linkedBranches;
757
+ if (!connection || !Array.isArray(connection.nodes) || typeof connection.pageInfo?.hasNextPage !== 'boolean')
758
+ throw new DeliveryError('Incomplete GitHub linked issue branch readback.');
759
+ for (const node of connection.nodes) {
760
+ const ref = node?.ref;
761
+ if (!ref || !ref.name || !/^[a-f0-9]{40}$/u.test(ref.target.oid) || !ref.repository.nameWithOwner)
762
+ throw new DeliveryError('Incomplete GitHub linked branch identity.');
763
+ if (ref.repository.nameWithOwner.toLowerCase() === context.config.repository.toLowerCase())
764
+ branches.push({ name: ref.name, sha: ref.target.oid, repository: ref.repository.nameWithOwner });
898
765
  }
899
- const assertAuthority = () => assertNativeWorktreeAcceptance(context, {
900
- repository: plan.repository,
901
- subject: issueNumber,
902
- operator: plan.operator,
903
- reviewerActor: plan.reviewerActor,
904
- ...ids,
905
- operatorBody: continuationOperatorBody(plan),
906
- reviewerBody: continuationReviewerBody(plan, ids.authorityCommentId),
907
- revokedBody: stableJson({
908
- schemaVersion: 'ai-delivery.worktree-continuation-revocation@1',
909
- planId: plan.planId,
910
- authorityCommentId: ids.authorityCommentId,
911
- }),
912
- });
913
- const assertPreservedSource = async () => {
914
- const issue = (await context.clients.rest.issues.get({ ...context.repo, issue_number: issueNumber })).data;
915
- if (issue.state !== 'open' || !(await readyCheck(context, issueNumber)).ready)
916
- throw new DeliveryError('Committed continuation requires an open, ready unfinished issue.');
917
- assertIssueWorktreeLocation(original, context.root);
918
- assertClean(original.path);
919
- if (git(original.path, 'rev-parse', 'HEAD') !== plan.head.sha ||
920
- git(original.path, 'rev-parse', 'HEAD^{tree}') !== plan.head.tree)
921
- throw new DeliveryError('Committed continuation source changed during exact-plan recovery.');
922
- const fresh = await readMergedContinuationTerminal(context, original, plan.operator);
923
- if (fresh.terminal.mergeId !== plan.terminalMergeId ||
924
- stableJson([...fresh.preserved, registry.ownerWitness]) !== stableJson(plan.preserved))
925
- throw new DeliveryError('Committed continuation original lineage changed.');
926
- await assertAuthority();
927
- if (stableJson(await readRuntime()) !== stableJson(plan.currentRuntime))
928
- throw new DeliveryError('Committed continuation runtime admission changed during exact-plan recovery.');
929
- };
930
- await assertPreservedSource();
931
- registry.begin(plan, ids);
932
- await assertPreservedSource();
933
- registry.commit();
934
- await assertPreservedSource();
935
- registry.complete();
936
- }));
937
- }
938
- export async function developIssue(context, issueNumber, runtimeEntryPath) {
939
- await preflightReviewRoute(context, undefined, undefined, 'development');
940
- const initialIssue = (await context.clients.rest.issues.get({ issue_number: issueNumber, ...context.repo })).data;
941
- if (initialIssue.state === 'closed')
942
- throw new DeliveryError(`Issue #${issueNumber} is closed. Reopen it before developing.`);
943
- const readiness = await readyCheck(context, issueNumber);
944
- if (!readiness.ready)
945
- throw new DeliveryError(`Issue #${issueNumber} is not ready: ${readiness.failures.map((f) => f.message).join('; ')}`);
946
- const registered = listWorktreesStrict(context.root).filter((entry) => entry.type === 'issue' && entry.issueNumber === issueNumber);
947
- if (registered.length > 1)
948
- throw new DeliveryError('Issue has duplicate worktree registry rows.');
949
- const pending = pendingCommittedContinuation(issueNumber, context.root);
950
- const selected = registered[0];
951
- if (pending ||
952
- (selected?.status === 'merged' &&
953
- context.clients.authSource === 'personal' &&
954
- !existsSync(join(gitCommonDir(selected.path), 'ai-delivery', 'merges', String(issueNumber), `${git(selected.path, 'rev-parse', 'HEAD')}.json`)))) {
955
- if (!selected)
956
- throw new DeliveryError('Committed continuation canonical custody is missing.');
957
- await continueCommittedDescendant(context, issueNumber, selected, runtimeEntryPath);
958
- }
959
- else if (selected?.status === 'merged') {
960
- const previous = getIssueWorktreeStrict(issueNumber, context.root);
961
- if (previous.identity !== context.config.roles.author.identity || previous.prNumber === undefined)
962
- throw new DeliveryError('Merged continuation requires the same preparing owner and exact prior PR.');
963
- assertIssueWorktreeLocation(previous, context.root);
964
- assertClean(previous.path);
965
- const head = git(previous.path, 'rev-parse', 'HEAD');
966
- if (!existsSync(join(gitCommonDir(previous.path), 'ai-delivery', 'merges', String(issueNumber), `${head}.json`)))
967
- throw new DeliveryError('Merged continuation lacks its exact terminal merge receipt.');
968
- await withRuntimeSetupWriter(previous.path, async (writer) => {
969
- writer.assertQuiescent();
970
- // Existing terminal recovery validates the historical run, publication, intent and remote merge.
971
- await mergePr(context, { issueNumber, prNumber: previous.prNumber });
972
- const currentIssue = (await context.clients.rest.issues.get({ issue_number: issueNumber, ...context.repo })).data;
973
- if (currentIssue.state !== 'open' || !(await readyCheck(context, issueNumber)).ready)
974
- throw new DeliveryError('Merged continuation requires an open, ready unfinished issue.');
975
- assertIssueWorktreeLocation(previous, context.root);
976
- assertClean(previous.path);
977
- if (git(previous.path, 'rev-parse', 'HEAD') !== head)
978
- throw new DeliveryError('Merged continuation source changed during terminal readback.');
979
- const { prNumber: _priorPr, ...sameOwner } = previous;
980
- await addWorktreeEntry({ ...sameOwner, status: 'active', updatedAt: new Date().toISOString() }, context.root);
981
- writer.assertQuiescent();
982
- });
766
+ if (!connection.pageInfo.hasNextPage)
767
+ return branches;
768
+ const next = connection.pageInfo.endCursor;
769
+ if (!next || cursors.has(next))
770
+ throw new DeliveryError('GitHub linked branch pagination did not advance.');
771
+ cursors.add(next);
772
+ cursor = next;
983
773
  }
984
- const row = await prepareIssueWorktree({
985
- ...(context.configuration?.remote ? { remote: context.configuration.remote } : {}),
986
- identity: context.config.roles.author.identity,
987
- issueNumber,
988
- repoRoot: context.root,
989
- });
990
- const issue = (await context.clients.rest.issues.get({ issue_number: issueNumber, ...context.repo })).data;
991
- await syncIssueProjectStatus({
992
- graphql: context.clients.graphql,
993
- issueNodeId: issue.node_id,
994
- org: context.config.native.organization,
995
- ...(context.projectConfiguration ? { configuration: context.projectConfiguration } : {}),
996
- settings: projectSettingsFromDeliveryConfig(context.config),
997
- status: 'In Progress',
774
+ throw new DeliveryError('GitHub linked issue branches exceed the bounded readback window.');
775
+ }
776
+ export async function issueBranch(context, issueNumber, selected) {
777
+ const branches = (await linkedIssueBranches(context, issueNumber)).filter((branch) => selected === undefined || branch.name === selected);
778
+ if (branches.length !== 1)
779
+ throw new DeliveryError('Select exactly one GitHub-linked issue branch; no local worktree or receipt fallback is available.');
780
+ return branches[0].name;
781
+ }
782
+ export async function startIssueBranch(context, issueNumber, selected) {
783
+ const issue = (await context.clients.rest.issues.get({ ...context.repo, issue_number: issueNumber })).data;
784
+ if (issue.number !== issueNumber || 'pull_request' in issue || issue.state !== 'open')
785
+ throw new DeliveryError('Start requires the requested open GitHub issue.');
786
+ const branches = await linkedIssueBranches(context, issueNumber);
787
+ const name = selected ?? (branches.length === 1 ? branches[0].name : `issue/${issueNumber}`);
788
+ if (!/^(?!.*(?:\.\.|@\{|\/\/))[A-Za-z0-9._/-]+$/u.test(name) ||
789
+ name.startsWith('/') ||
790
+ name.endsWith('/') ||
791
+ name.endsWith('.lock'))
792
+ throw new DeliveryError('Invalid issue branch name.');
793
+ const existing = branches.filter((branch) => branch.name === name);
794
+ if (branches.length > 0 && existing.length !== 1)
795
+ throw new DeliveryError('Select exactly one existing GitHub-linked issue branch before start.');
796
+ if (existing.length > 1)
797
+ throw new DeliveryError('GitHub issue branch has conflicting linked matches.');
798
+ if (existing.length === 1)
799
+ return { issueNumber, issueUrl: issue.html_url, branch: name, headSha: existing[0].sha, reused: true };
800
+ const repository = (await context.clients.rest.repos.get({ ...context.repo })).data;
801
+ const base = (await context.clients.rest.git.getRef({ ...context.repo, ref: `heads/${repository.default_branch}` }))
802
+ .data.object.sha;
803
+ if (!/^[a-f0-9]{40}$/u.test(base) || !repository.node_id)
804
+ throw new DeliveryError('GitHub repository default branch identity is incomplete.');
805
+ await context.clients.graphql('mutation DeliveryStartBranch($input:CreateLinkedBranchInput!){createLinkedBranch(input:$input){linkedBranch{ref{name target{oid}}}}}', {
806
+ input: {
807
+ issueId: issue.node_id,
808
+ repositoryId: repository.node_id,
809
+ oid: base,
810
+ name,
811
+ clientMutationId: digestValue({ repository: context.config.repository, issueNumber, name, base }),
812
+ },
998
813
  });
999
- await journalIssueStart(context, issueNumber);
1000
- return { branch: row.branch, path: row.path };
814
+ const readback = (await linkedIssueBranches(context, issueNumber)).filter((branch) => branch.name === name);
815
+ if (readback.length !== 1 || readback[0].sha !== base)
816
+ throw new DeliveryError('Started GitHub branch/link readback disagrees; resume this issue without creating another.');
817
+ return { issueNumber, issueUrl: issue.html_url, branch: name, headSha: base, reused: false };
1001
818
  }
1002
819
  //# sourceMappingURL=issue.js.map