@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.
- package/CONTRIBUTING.md +9 -0
- package/README.md +56 -447
- package/dist/agent.d.ts +5 -11
- package/dist/agent.js +4 -8
- package/dist/agent.js.map +1 -1
- package/dist/cli.js +12 -107
- package/dist/cli.js.map +1 -1
- package/dist/config/deliveryConfig.d.ts +44 -55
- package/dist/config/deliveryConfig.js +85 -88
- package/dist/config/deliveryConfig.js.map +1 -1
- package/dist/config/deliveryConfig.test.js +13 -5
- package/dist/config/deliveryConfig.test.js.map +1 -1
- package/dist/delivery/common.js +7 -2
- package/dist/delivery/common.js.map +1 -1
- package/dist/delivery/delivery.test.js +2 -3
- package/dist/delivery/delivery.test.js.map +1 -1
- package/dist/delivery/index.d.ts +0 -6
- package/dist/delivery/index.js +0 -3
- package/dist/delivery/index.js.map +1 -1
- package/dist/delivery/legacy.d.ts +8 -0
- package/dist/delivery/legacy.js +5 -0
- package/dist/delivery/legacy.js.map +1 -0
- package/dist/delivery/policy.js +2 -13
- package/dist/delivery/policy.js.map +1 -1
- package/dist/directoryIndependent.test.d.ts +1 -0
- package/dist/directoryIndependent.test.js +262 -0
- package/dist/directoryIndependent.test.js.map +1 -0
- package/dist/dispatch.d.ts +29 -3
- package/dist/dispatch.js +160 -457
- package/dist/dispatch.js.map +1 -1
- package/dist/genericCompatibility.test.js +0 -35
- package/dist/genericCompatibility.test.js.map +1 -1
- package/dist/git.js +10 -3
- package/dist/git.js.map +1 -1
- package/dist/gitProcess.d.ts +6 -0
- package/dist/gitProcess.js +56 -0
- package/dist/gitProcess.js.map +1 -0
- package/dist/github/discovery.d.ts +1 -0
- package/dist/github/discovery.js +1 -1
- package/dist/github/discovery.js.map +1 -1
- package/dist/github/discovery.test.js +25 -20
- package/dist/github/discovery.test.js.map +1 -1
- package/dist/github/nativeIssueMetadata.d.ts +2 -2
- package/dist/github/nativeIssueMetadata.js.map +1 -1
- package/dist/github/projectDelivery.d.ts +1 -1
- package/dist/github/projectDelivery.js.map +1 -1
- package/dist/github/repo.d.ts +7 -1
- package/dist/github/repo.js +58 -2
- package/dist/github/repo.js.map +1 -1
- package/dist/issue.d.ts +11 -6
- package/dist/issue.js +136 -319
- package/dist/issue.js.map +1 -1
- package/dist/issueJournal.d.ts +1 -1
- package/dist/lifecycle.test.js +40 -7371
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/logger.js +4 -4
- package/dist/logger.js.map +1 -1
- package/dist/mcp/tools.d.ts +26 -42
- package/dist/mcp/tools.js +33 -42
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pluginInstaller.d.ts +1 -1
- package/dist/pluginPackaging.test.js +129 -0
- package/dist/pluginPackaging.test.js.map +1 -1
- package/dist/pr.d.ts +66 -114
- package/dist/pr.js +288 -1108
- package/dist/pr.js.map +1 -1
- package/dist/pr.test.js +295 -237
- package/dist/pr.test.js.map +1 -1
- package/dist/releaseReconciliation.test.d.ts +1 -0
- package/dist/releaseReconciliation.test.js +314 -0
- package/dist/releaseReconciliation.test.js.map +1 -0
- package/dist/review.d.ts +37 -7
- package/dist/review.js +106 -45
- package/dist/review.js.map +1 -1
- package/dist/review.test.js +30 -71
- package/dist/review.test.js.map +1 -1
- package/dist/services/agentReadinessService.d.ts +1 -1
- package/dist/services/agentReadinessService.js +2 -0
- package/dist/services/agentReadinessService.js.map +1 -1
- package/dist/services/deliveryAdmission.d.ts +4 -4
- package/dist/services/deliveryAdmission.js.map +1 -1
- package/dist/setup.test.js +10 -5
- package/dist/setup.test.js.map +1 -1
- package/dist/start.test.d.ts +1 -0
- package/dist/start.test.js +72 -0
- package/dist/start.test.js.map +1 -0
- package/dist/stdoutRegression.test.d.ts +1 -0
- package/dist/stdoutRegression.test.js +326 -0
- package/dist/stdoutRegression.test.js.map +1 -0
- package/dist/verification.d.ts +1 -27
- package/dist/verification.js +5 -1332
- package/dist/verification.js.map +1 -1
- package/dist/worktree.d.ts +1 -31
- package/dist/worktree.js +5 -200
- package/dist/worktree.js.map +1 -1
- package/dist/worktreeTransition.d.ts +1 -1
- package/dist/worktreeTransition.js +10 -5
- package/dist/worktreeTransition.js.map +1 -1
- package/dist/worktreeTransition.test.js +2 -3
- package/dist/worktreeTransition.test.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ai-delivery/.claude-plugin/plugin.json +2 -2
- package/plugins/ai-delivery/README.md +3 -3
- package/plugins/ai-delivery/plugin.json +1 -1
- package/plugins/ai-delivery/runtime/dist/cli.js +50 -59
- package/plugins/ai-delivery/runtime/package.json +1 -1
- package/plugins/ai-delivery/skills/intake-create/SKILL.md +1 -1
- package/plugins/ai-delivery/skills/pr-handoff/SKILL.md +14 -21
- package/plugins/ai-delivery/skills/worktree-lifecycle/SKILL.md +12 -23
- package/dist/services/deliveryRecordService.d.ts +0 -47
- package/dist/services/deliveryRecordService.js +0 -199
- 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 {
|
|
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 {
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
103
|
-
|
|
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
|
|
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:
|
|
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) =>
|
|
133
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
745
|
+
repoRoot: undefined,
|
|
783
746
|
title: info.title,
|
|
784
747
|
trackingParent,
|
|
785
748
|
});
|
|
786
749
|
}
|
|
787
|
-
function
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
|
1000
|
-
|
|
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
|