dev-loops 0.1.0
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/.pi/dev-loop/defaults.yaml +477 -0
- package/AGENTS.md +25 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/agents/dev-loop.agent.md +82 -0
- package/agents/developer.agent.md +37 -0
- package/agents/docs.agent.md +33 -0
- package/agents/fixer.agent.md +53 -0
- package/agents/quality.agent.md +28 -0
- package/agents/refiner.agent.md +87 -0
- package/agents/review.agent.md +64 -0
- package/cli/index.mjs +424 -0
- package/extension/README.md +233 -0
- package/extension/checks.ts +94 -0
- package/extension/index.ts +131 -0
- package/extension/post-merge-update.ts +512 -0
- package/extension/presentation.ts +107 -0
- package/lib/dev-loops-core.mjs +284 -0
- package/package.json +103 -0
- package/scripts/README.md +1007 -0
- package/scripts/_cli-primitives.mjs +10 -0
- package/scripts/_core-helpers.mjs +30 -0
- package/scripts/docs/validate-links.mjs +567 -0
- package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
- package/scripts/github/_review-thread-mutations.mjs +214 -0
- package/scripts/github/capture-review-threads.mjs +180 -0
- package/scripts/github/create-draft-pr.mjs +108 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
- package/scripts/github/detect-linked-issue-pr.mjs +331 -0
- package/scripts/github/manage-sub-issues.mjs +394 -0
- package/scripts/github/probe-copilot-review.mjs +323 -0
- package/scripts/github/ready-for-review.mjs +93 -0
- package/scripts/github/reconcile-draft-gate.mjs +328 -0
- package/scripts/github/reply-resolve-review-thread.mjs +42 -0
- package/scripts/github/reply-resolve-review-threads.mjs +329 -0
- package/scripts/github/request-copilot-review.mjs +551 -0
- package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
- package/scripts/github/stage-reviewer-draft.mjs +191 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
- package/scripts/github/verify-fresh-review-context.mjs +125 -0
- package/scripts/github/write-gate-findings-log.mjs +212 -0
- package/scripts/loop/_checkpoint-io.mjs +55 -0
- package/scripts/loop/_checkpoint-paths.mjs +28 -0
- package/scripts/loop/_handoff-contract.mjs +230 -0
- package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
- package/scripts/loop/_loop-evidence.mjs +32 -0
- package/scripts/loop/_pr-runner-coordination.mjs +611 -0
- package/scripts/loop/_stale-runner-detection.mjs +145 -0
- package/scripts/loop/_steering-state-file.mjs +134 -0
- package/scripts/loop/build-handoff-envelope.mjs +181 -0
- package/scripts/loop/checkpoint-contract.mjs +49 -0
- package/scripts/loop/conductor-monitor.mjs +1850 -0
- package/scripts/loop/conductor.mjs +214 -0
- package/scripts/loop/copilot-pr-handoff.mjs +493 -0
- package/scripts/loop/debt-remediate.mjs +304 -0
- package/scripts/loop/detect-change-scope.mjs +102 -0
- package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
- package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
- package/scripts/loop/detect-internal-only-pr.mjs +270 -0
- package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
- package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
- package/scripts/loop/detect-stale-runner.mjs +250 -0
- package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
- package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
- package/scripts/loop/info.mjs +267 -0
- package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
- package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
- package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
- package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
- package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
- package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
- package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
- package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
- package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
- package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
- package/scripts/loop/inspect-run-viewer.mjs +82 -0
- package/scripts/loop/inspect-run.mjs +382 -0
- package/scripts/loop/outer-loop.mjs +419 -0
- package/scripts/loop/pr-runner-coordination.mjs +143 -0
- package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
- package/scripts/loop/pre-flight-gate.mjs +236 -0
- package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
- package/scripts/loop/pre-push-main-guard.mjs +103 -0
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
- package/scripts/loop/print-gates.mjs +42 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
- package/scripts/loop/run-conductor-cycle.mjs +322 -0
- package/scripts/loop/run-queue.mjs +124 -0
- package/scripts/loop/run-refinement-audit.mjs +513 -0
- package/scripts/loop/run-watch-cycle.mjs +358 -0
- package/scripts/loop/steer-loop.mjs +841 -0
- package/scripts/loop/ui-designer-review-contract.mjs +76 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
- package/scripts/projects/add-queue-item.mjs +528 -0
- package/scripts/projects/ensure-queue-board.mjs +837 -0
- package/scripts/projects/list-queue-items.mjs +489 -0
- package/scripts/projects/move-queue-item.mjs +549 -0
- package/scripts/projects/reorder-queue-item.mjs +518 -0
- package/scripts/refine/_refine-helpers.mjs +258 -0
- package/scripts/refine/prose-linkage-detector.mjs +92 -0
- package/scripts/refine/refinement-completeness-checker.mjs +88 -0
- package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
- package/scripts/refine/tree-integrity-validator.mjs +211 -0
- package/scripts/refine/verify.mjs +178 -0
- package/scripts/repo-wiki-local.mjs +156 -0
- package/scripts/repo-wiki.mjs +119 -0
- package/skills/copilot-pr-followup/SKILL.md +380 -0
- package/skills/dev-loop/SKILL.md +141 -0
- package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
- package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
- package/skills/dev-loop/scripts/init-phase.mjs +71 -0
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
- package/skills/dev-loop/scripts/phase-files.mjs +29 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
- package/skills/dev-loop/scripts/render-template.mjs +82 -0
- package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
- package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
- package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
- package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
- package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
- package/skills/dev-loop/templates/dev-mode-review.md +17 -0
- package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
- package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
- package/skills/dev-loop/templates/phase-doc.md +27 -0
- package/skills/dev-loop/templates/phase-summary.md +13 -0
- package/skills/dev-loop/templates/phase-variant.md +15 -0
- package/skills/dev-loop/templates/retrospective.md +11 -0
- package/skills/dev-loop/templates/review.md +32 -0
- package/skills/dev-loop/templates/ui-vision-review.md +55 -0
- package/skills/docs/acceptance-criteria-verification.md +21 -0
- package/skills/docs/anti-patterns.md +21 -0
- package/skills/docs/artifact-authority-contract.md +119 -0
- package/skills/docs/confirmation-rules.md +28 -0
- package/skills/docs/copilot-ci-status-contract.md +52 -0
- package/skills/docs/copilot-loop-operations.md +233 -0
- package/skills/docs/debt-remediation-contract.md +107 -0
- package/skills/docs/entrypoint-strategies.md +115 -0
- package/skills/docs/epic-tree-refinement-procedure.md +234 -0
- package/skills/docs/issue-intake-procedure.md +235 -0
- package/skills/docs/main-agent-contract.md +72 -0
- package/skills/docs/merge-preconditions.md +29 -0
- package/skills/docs/pr-lifecycle-contract.md +209 -0
- package/skills/docs/public-dev-loop-contract.md +497 -0
- package/skills/docs/retrospective-checkpoint-contract.md +159 -0
- package/skills/docs/stop-conditions.md +29 -0
- package/skills/docs/structural-quality.md +42 -0
- package/skills/docs/tracker-first-loop-state.md +281 -0
- package/skills/docs/validation-policy.md +27 -0
- package/skills/docs/workflow-handoff-contract.md +135 -0
- package/skills/final-approval/SKILL.md +19 -0
- package/skills/local-implementation/SKILL.md +640 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
+
import { parseIssueNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
|
+
export const LINKED_ISSUE_PR_QUERY = [
|
|
6
|
+
"query($owner:String!, $name:String!, $issue:Int!, $after:String) {",
|
|
7
|
+
" repository(owner:$owner, name:$name) {",
|
|
8
|
+
" issue(number:$issue) {",
|
|
9
|
+
" timelineItems(first:100, after:$after, itemTypes:[CONNECTED_EVENT, CROSS_REFERENCED_EVENT]) {",
|
|
10
|
+
" pageInfo {",
|
|
11
|
+
" hasNextPage",
|
|
12
|
+
" endCursor",
|
|
13
|
+
" }",
|
|
14
|
+
" nodes {",
|
|
15
|
+
" __typename",
|
|
16
|
+
" ... on ConnectedEvent {",
|
|
17
|
+
" createdAt",
|
|
18
|
+
" subject {",
|
|
19
|
+
" __typename",
|
|
20
|
+
" ... on PullRequest {",
|
|
21
|
+
" number",
|
|
22
|
+
" state",
|
|
23
|
+
" url",
|
|
24
|
+
" repository { nameWithOwner }",
|
|
25
|
+
" }",
|
|
26
|
+
" }",
|
|
27
|
+
" }",
|
|
28
|
+
" ... on CrossReferencedEvent {",
|
|
29
|
+
" createdAt",
|
|
30
|
+
" source {",
|
|
31
|
+
" __typename",
|
|
32
|
+
" ... on PullRequest {",
|
|
33
|
+
" number",
|
|
34
|
+
" state",
|
|
35
|
+
" url",
|
|
36
|
+
" repository { nameWithOwner }",
|
|
37
|
+
" }",
|
|
38
|
+
" }",
|
|
39
|
+
" }",
|
|
40
|
+
" }",
|
|
41
|
+
" }",
|
|
42
|
+
" }",
|
|
43
|
+
" }",
|
|
44
|
+
"}",
|
|
45
|
+
].join("\n");
|
|
46
|
+
const USAGE = `Usage: detect-linked-issue-pr.mjs --repo <owner/name> --issue <number>
|
|
47
|
+
Detect whether an issue already has an open linked pull request in the same repository.
|
|
48
|
+
This helper owns linked-event query pagination and deterministic selection.
|
|
49
|
+
Required:
|
|
50
|
+
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
51
|
+
--issue <number> Issue number
|
|
52
|
+
Success output (stdout, JSON):
|
|
53
|
+
{
|
|
54
|
+
"ok": true,
|
|
55
|
+
"repo": "owner/name",
|
|
56
|
+
"issue": 85,
|
|
57
|
+
"hasOpenLinkedPr": true|false,
|
|
58
|
+
"prNumber": 90|null,
|
|
59
|
+
"prUrl": "..."|null,
|
|
60
|
+
"selection"?: {
|
|
61
|
+
"eventType": "CONNECTED_EVENT"|"CROSS_REFERENCED_EVENT",
|
|
62
|
+
"eventCreatedAt": "..."
|
|
63
|
+
},
|
|
64
|
+
"hasPriorClosedUnmergedPr"?: true|false,
|
|
65
|
+
"priorClosedUnmergedPrNumber"?: 149|null,
|
|
66
|
+
"priorClosedUnmergedPrUrl"?: "..."|null
|
|
67
|
+
}
|
|
68
|
+
When hasOpenLinkedPr is false, the output also includes hasPriorClosedUnmergedPr,
|
|
69
|
+
priorClosedUnmergedPrNumber, and priorClosedUnmergedPrUrl reflecting any same-repo
|
|
70
|
+
linked PR that was closed without merging.
|
|
71
|
+
Error output (stderr, JSON):
|
|
72
|
+
Argument/usage errors:
|
|
73
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
74
|
+
gh/runtime failures:
|
|
75
|
+
{ "ok": false, "error": "..." }`.trim();
|
|
76
|
+
const parseError = buildParseError(USAGE);
|
|
77
|
+
export function parseDetectLinkedIssuePrCliArgs(argv) {
|
|
78
|
+
const args = [...argv];
|
|
79
|
+
const options = {
|
|
80
|
+
help: false,
|
|
81
|
+
repo: undefined,
|
|
82
|
+
issue: undefined,
|
|
83
|
+
};
|
|
84
|
+
while (args.length > 0) {
|
|
85
|
+
const token = args.shift();
|
|
86
|
+
if (token === "--help" || token === "-h") {
|
|
87
|
+
options.help = true;
|
|
88
|
+
return options;
|
|
89
|
+
}
|
|
90
|
+
if (token === "--repo") {
|
|
91
|
+
options.repo = requireOptionValue(args, "--repo", parseError).trim();
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (token === "--issue") {
|
|
95
|
+
options.issue = parseIssueNumber(requireOptionValue(args, "--issue", parseError), parseError);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
throw parseError(`Unknown argument: ${token}`);
|
|
99
|
+
}
|
|
100
|
+
if (options.repo === undefined || options.issue === undefined) {
|
|
101
|
+
throw parseError("Linked PR detection requires both --repo <owner/name> and --issue <number>");
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
parseRepoSlug(options.repo);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
107
|
+
}
|
|
108
|
+
return options;
|
|
109
|
+
}
|
|
110
|
+
function buildQueryArgs({ owner, name, issue, after }) {
|
|
111
|
+
const args = [
|
|
112
|
+
"api",
|
|
113
|
+
"graphql",
|
|
114
|
+
"--field",
|
|
115
|
+
`owner=${owner}`,
|
|
116
|
+
"--field",
|
|
117
|
+
`name=${name}`,
|
|
118
|
+
"-F",
|
|
119
|
+
`issue=${issue}`,
|
|
120
|
+
"--field",
|
|
121
|
+
`query=${LINKED_ISSUE_PR_QUERY}`,
|
|
122
|
+
];
|
|
123
|
+
if (typeof after === "string" && after.length > 0) {
|
|
124
|
+
args.push("--field", `after=${after}`);
|
|
125
|
+
}
|
|
126
|
+
return args;
|
|
127
|
+
}
|
|
128
|
+
function readTimelineConnection(payload) {
|
|
129
|
+
const connection = payload?.data?.repository?.issue?.timelineItems;
|
|
130
|
+
if (!connection || typeof connection !== "object") {
|
|
131
|
+
throw new Error("Invalid linked-PR GraphQL payload: missing data.repository.issue.timelineItems");
|
|
132
|
+
}
|
|
133
|
+
const nodes = Array.isArray(connection.nodes) ? connection.nodes : [];
|
|
134
|
+
const pageInfo = connection.pageInfo ?? {};
|
|
135
|
+
return {
|
|
136
|
+
nodes,
|
|
137
|
+
hasNextPage: Boolean(pageInfo.hasNextPage),
|
|
138
|
+
endCursor: typeof pageInfo.endCursor === "string" ? pageInfo.endCursor : null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function normalizeLinkedPrNode(node) {
|
|
142
|
+
if (!node || typeof node !== "object") {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (node.__typename === "ConnectedEvent") {
|
|
146
|
+
return {
|
|
147
|
+
eventType: "CONNECTED_EVENT",
|
|
148
|
+
eventCreatedAt: node.createdAt,
|
|
149
|
+
pr: node.subject,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (node.__typename === "CrossReferencedEvent") {
|
|
153
|
+
return {
|
|
154
|
+
eventType: "CROSS_REFERENCED_EVENT",
|
|
155
|
+
eventCreatedAt: node.createdAt,
|
|
156
|
+
pr: node.source,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function compareStableStrings(left, right) {
|
|
162
|
+
if (left === right) {
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
return left < right ? -1 : 1;
|
|
166
|
+
}
|
|
167
|
+
function normalizeRepoSlugForComparison(repo) {
|
|
168
|
+
return typeof repo === "string" ? repo.trim().toLowerCase() : "";
|
|
169
|
+
}
|
|
170
|
+
function normalizeOpenSameRepoCandidate(candidate, repo) {
|
|
171
|
+
const pr = candidate?.pr;
|
|
172
|
+
const number = pr?.number;
|
|
173
|
+
const state = pr?.state;
|
|
174
|
+
const url = pr?.url;
|
|
175
|
+
const nameWithOwner = pr?.repository?.nameWithOwner;
|
|
176
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (
|
|
180
|
+
state !== "OPEN"
|
|
181
|
+
|| normalizeRepoSlugForComparison(nameWithOwner) !== normalizeRepoSlugForComparison(repo)
|
|
182
|
+
) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const createdAtMs = Date.parse(candidate.eventCreatedAt);
|
|
186
|
+
if (!Number.isFinite(createdAtMs)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
prNumber: number,
|
|
191
|
+
prUrl: typeof url === "string" ? url : null,
|
|
192
|
+
eventType: candidate.eventType,
|
|
193
|
+
eventCreatedAt: typeof candidate.eventCreatedAt === "string" ? candidate.eventCreatedAt : null,
|
|
194
|
+
createdAtMs,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function normalizeClosedUnmergedSameRepoCandidate(candidate, repo) {
|
|
198
|
+
const pr = candidate?.pr;
|
|
199
|
+
const number = pr?.number;
|
|
200
|
+
const state = pr?.state;
|
|
201
|
+
const url = pr?.url;
|
|
202
|
+
const nameWithOwner = pr?.repository?.nameWithOwner;
|
|
203
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
state !== "CLOSED"
|
|
208
|
+
|| normalizeRepoSlugForComparison(nameWithOwner) !== normalizeRepoSlugForComparison(repo)
|
|
209
|
+
) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const createdAtMs = Date.parse(candidate.eventCreatedAt);
|
|
213
|
+
if (!Number.isFinite(createdAtMs)) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
prNumber: number,
|
|
218
|
+
prUrl: typeof url === "string" ? url : null,
|
|
219
|
+
eventType: candidate.eventType,
|
|
220
|
+
eventCreatedAt: typeof candidate.eventCreatedAt === "string" ? candidate.eventCreatedAt : null,
|
|
221
|
+
createdAtMs,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
export function selectLinkedIssuePr(candidates) {
|
|
225
|
+
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const sorted = [...candidates].sort((left, right) => {
|
|
229
|
+
const leftPriority = left.eventType === "CONNECTED_EVENT" ? 0 : 1;
|
|
230
|
+
const rightPriority = right.eventType === "CONNECTED_EVENT" ? 0 : 1;
|
|
231
|
+
if (leftPriority !== rightPriority) {
|
|
232
|
+
return leftPriority - rightPriority;
|
|
233
|
+
}
|
|
234
|
+
if (left.createdAtMs !== right.createdAtMs) {
|
|
235
|
+
return right.createdAtMs - left.createdAtMs;
|
|
236
|
+
}
|
|
237
|
+
if (left.prNumber !== right.prNumber) {
|
|
238
|
+
return right.prNumber - left.prNumber;
|
|
239
|
+
}
|
|
240
|
+
return compareStableStrings(String(left.prUrl ?? ""), String(right.prUrl ?? ""));
|
|
241
|
+
});
|
|
242
|
+
return sorted[0] ?? null;
|
|
243
|
+
}
|
|
244
|
+
export async function detectLinkedIssuePr({ repo, issue }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
245
|
+
const { owner, name } = parseRepoSlug(repo);
|
|
246
|
+
const candidates = [];
|
|
247
|
+
const closedUnmergedCandidates = [];
|
|
248
|
+
let after = null;
|
|
249
|
+
while (true) {
|
|
250
|
+
const result = await runChild(
|
|
251
|
+
ghCommand,
|
|
252
|
+
buildQueryArgs({ owner, name, issue, after }),
|
|
253
|
+
env,
|
|
254
|
+
);
|
|
255
|
+
if (result.code !== 0) {
|
|
256
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
257
|
+
throw new Error(`gh command failed: ${detail}`);
|
|
258
|
+
}
|
|
259
|
+
const payload = parseJsonText(result.stdout);
|
|
260
|
+
const { nodes, hasNextPage, endCursor } = readTimelineConnection(payload);
|
|
261
|
+
for (const node of nodes) {
|
|
262
|
+
const normalizedNode = normalizeLinkedPrNode(node);
|
|
263
|
+
if (!normalizedNode) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const normalizedCandidate = normalizeOpenSameRepoCandidate(normalizedNode, repo);
|
|
267
|
+
if (normalizedCandidate) {
|
|
268
|
+
candidates.push(normalizedCandidate);
|
|
269
|
+
}
|
|
270
|
+
const closedUnmergedCandidate = normalizeClosedUnmergedSameRepoCandidate(normalizedNode, repo);
|
|
271
|
+
if (closedUnmergedCandidate) {
|
|
272
|
+
closedUnmergedCandidates.push(closedUnmergedCandidate);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!hasNextPage) {
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
if (!endCursor) {
|
|
279
|
+
throw new Error("Invalid linked-PR GraphQL payload: pageInfo.hasNextPage is true but endCursor is missing");
|
|
280
|
+
}
|
|
281
|
+
after = endCursor;
|
|
282
|
+
}
|
|
283
|
+
const selected = selectLinkedIssuePr(candidates);
|
|
284
|
+
const selectedClosedUnmerged = selectLinkedIssuePr(closedUnmergedCandidates);
|
|
285
|
+
if (!selected) {
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
repo,
|
|
289
|
+
issue,
|
|
290
|
+
hasOpenLinkedPr: false,
|
|
291
|
+
prNumber: null,
|
|
292
|
+
prUrl: null,
|
|
293
|
+
hasPriorClosedUnmergedPr: selectedClosedUnmerged !== null,
|
|
294
|
+
priorClosedUnmergedPrNumber: selectedClosedUnmerged?.prNumber ?? null,
|
|
295
|
+
priorClosedUnmergedPrUrl: selectedClosedUnmerged?.prUrl ?? null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
ok: true,
|
|
300
|
+
repo,
|
|
301
|
+
issue,
|
|
302
|
+
hasOpenLinkedPr: true,
|
|
303
|
+
prNumber: selected.prNumber,
|
|
304
|
+
prUrl: selected.prUrl,
|
|
305
|
+
selection: {
|
|
306
|
+
eventType: selected.eventType,
|
|
307
|
+
eventCreatedAt: selected.eventCreatedAt,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export async function runCli(
|
|
312
|
+
argv = process.argv.slice(2),
|
|
313
|
+
{ stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
|
|
314
|
+
) {
|
|
315
|
+
const options = parseDetectLinkedIssuePrCliArgs(argv);
|
|
316
|
+
if (options.help) {
|
|
317
|
+
stdout.write(`${USAGE}\n`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const result = await detectLinkedIssuePr(
|
|
321
|
+
{ repo: options.repo, issue: options.issue },
|
|
322
|
+
{ env, ghCommand },
|
|
323
|
+
);
|
|
324
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
325
|
+
}
|
|
326
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
327
|
+
runCli().catch((error) => {
|
|
328
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
329
|
+
process.exitCode = 1;
|
|
330
|
+
});
|
|
331
|
+
}
|