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.
Files changed (156) hide show
  1. package/.pi/dev-loop/defaults.yaml +477 -0
  2. package/AGENTS.md +25 -0
  3. package/CHANGELOG.md +18 -0
  4. package/LICENSE +21 -0
  5. package/README.md +178 -0
  6. package/agents/dev-loop.agent.md +82 -0
  7. package/agents/developer.agent.md +37 -0
  8. package/agents/docs.agent.md +33 -0
  9. package/agents/fixer.agent.md +53 -0
  10. package/agents/quality.agent.md +28 -0
  11. package/agents/refiner.agent.md +87 -0
  12. package/agents/review.agent.md +64 -0
  13. package/cli/index.mjs +424 -0
  14. package/extension/README.md +233 -0
  15. package/extension/checks.ts +94 -0
  16. package/extension/index.ts +131 -0
  17. package/extension/post-merge-update.ts +512 -0
  18. package/extension/presentation.ts +107 -0
  19. package/lib/dev-loops-core.mjs +284 -0
  20. package/package.json +103 -0
  21. package/scripts/README.md +1007 -0
  22. package/scripts/_cli-primitives.mjs +10 -0
  23. package/scripts/_core-helpers.mjs +30 -0
  24. package/scripts/docs/validate-links.mjs +567 -0
  25. package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
  26. package/scripts/github/_review-thread-mutations.mjs +214 -0
  27. package/scripts/github/capture-review-threads.mjs +180 -0
  28. package/scripts/github/create-draft-pr.mjs +108 -0
  29. package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
  30. package/scripts/github/detect-linked-issue-pr.mjs +331 -0
  31. package/scripts/github/manage-sub-issues.mjs +394 -0
  32. package/scripts/github/probe-copilot-review.mjs +323 -0
  33. package/scripts/github/ready-for-review.mjs +93 -0
  34. package/scripts/github/reconcile-draft-gate.mjs +328 -0
  35. package/scripts/github/reply-resolve-review-thread.mjs +42 -0
  36. package/scripts/github/reply-resolve-review-threads.mjs +329 -0
  37. package/scripts/github/request-copilot-review.mjs +551 -0
  38. package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
  39. package/scripts/github/stage-reviewer-draft.mjs +191 -0
  40. package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
  41. package/scripts/github/verify-fresh-review-context.mjs +125 -0
  42. package/scripts/github/write-gate-findings-log.mjs +212 -0
  43. package/scripts/loop/_checkpoint-io.mjs +55 -0
  44. package/scripts/loop/_checkpoint-paths.mjs +28 -0
  45. package/scripts/loop/_handoff-contract.mjs +230 -0
  46. package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
  47. package/scripts/loop/_loop-evidence.mjs +32 -0
  48. package/scripts/loop/_pr-runner-coordination.mjs +611 -0
  49. package/scripts/loop/_stale-runner-detection.mjs +145 -0
  50. package/scripts/loop/_steering-state-file.mjs +134 -0
  51. package/scripts/loop/build-handoff-envelope.mjs +181 -0
  52. package/scripts/loop/checkpoint-contract.mjs +49 -0
  53. package/scripts/loop/conductor-monitor.mjs +1850 -0
  54. package/scripts/loop/conductor.mjs +214 -0
  55. package/scripts/loop/copilot-pr-handoff.mjs +493 -0
  56. package/scripts/loop/debt-remediate.mjs +304 -0
  57. package/scripts/loop/detect-change-scope.mjs +102 -0
  58. package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
  59. package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
  60. package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
  61. package/scripts/loop/detect-internal-only-pr.mjs +270 -0
  62. package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
  63. package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
  64. package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
  65. package/scripts/loop/detect-stale-runner.mjs +250 -0
  66. package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
  67. package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
  68. package/scripts/loop/info.mjs +267 -0
  69. package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
  70. package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
  71. package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
  72. package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
  73. package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
  74. package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
  75. package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
  76. package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
  77. package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
  78. package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
  79. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
  80. package/scripts/loop/inspect-run-viewer.mjs +82 -0
  81. package/scripts/loop/inspect-run.mjs +382 -0
  82. package/scripts/loop/outer-loop.mjs +419 -0
  83. package/scripts/loop/pr-runner-coordination.mjs +143 -0
  84. package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
  85. package/scripts/loop/pre-flight-gate.mjs +236 -0
  86. package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
  87. package/scripts/loop/pre-push-main-guard.mjs +103 -0
  88. package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
  89. package/scripts/loop/print-gates.mjs +42 -0
  90. package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
  91. package/scripts/loop/run-conductor-cycle.mjs +322 -0
  92. package/scripts/loop/run-queue.mjs +124 -0
  93. package/scripts/loop/run-refinement-audit.mjs +513 -0
  94. package/scripts/loop/run-watch-cycle.mjs +358 -0
  95. package/scripts/loop/steer-loop.mjs +841 -0
  96. package/scripts/loop/ui-designer-review-contract.mjs +76 -0
  97. package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
  98. package/scripts/projects/add-queue-item.mjs +528 -0
  99. package/scripts/projects/ensure-queue-board.mjs +837 -0
  100. package/scripts/projects/list-queue-items.mjs +489 -0
  101. package/scripts/projects/move-queue-item.mjs +549 -0
  102. package/scripts/projects/reorder-queue-item.mjs +518 -0
  103. package/scripts/refine/_refine-helpers.mjs +258 -0
  104. package/scripts/refine/prose-linkage-detector.mjs +92 -0
  105. package/scripts/refine/refinement-completeness-checker.mjs +88 -0
  106. package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
  107. package/scripts/refine/tree-integrity-validator.mjs +211 -0
  108. package/scripts/refine/verify.mjs +178 -0
  109. package/scripts/repo-wiki-local.mjs +156 -0
  110. package/scripts/repo-wiki.mjs +119 -0
  111. package/skills/copilot-pr-followup/SKILL.md +380 -0
  112. package/skills/dev-loop/SKILL.md +141 -0
  113. package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
  114. package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
  115. package/skills/dev-loop/scripts/init-phase.mjs +71 -0
  116. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
  117. package/skills/dev-loop/scripts/phase-files.mjs +29 -0
  118. package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
  119. package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
  120. package/skills/dev-loop/scripts/render-template.mjs +82 -0
  121. package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
  122. package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
  123. package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
  124. package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
  125. package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
  126. package/skills/dev-loop/templates/dev-mode-review.md +17 -0
  127. package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
  128. package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
  129. package/skills/dev-loop/templates/phase-doc.md +27 -0
  130. package/skills/dev-loop/templates/phase-summary.md +13 -0
  131. package/skills/dev-loop/templates/phase-variant.md +15 -0
  132. package/skills/dev-loop/templates/retrospective.md +11 -0
  133. package/skills/dev-loop/templates/review.md +32 -0
  134. package/skills/dev-loop/templates/ui-vision-review.md +55 -0
  135. package/skills/docs/acceptance-criteria-verification.md +21 -0
  136. package/skills/docs/anti-patterns.md +21 -0
  137. package/skills/docs/artifact-authority-contract.md +119 -0
  138. package/skills/docs/confirmation-rules.md +28 -0
  139. package/skills/docs/copilot-ci-status-contract.md +52 -0
  140. package/skills/docs/copilot-loop-operations.md +233 -0
  141. package/skills/docs/debt-remediation-contract.md +107 -0
  142. package/skills/docs/entrypoint-strategies.md +115 -0
  143. package/skills/docs/epic-tree-refinement-procedure.md +234 -0
  144. package/skills/docs/issue-intake-procedure.md +235 -0
  145. package/skills/docs/main-agent-contract.md +72 -0
  146. package/skills/docs/merge-preconditions.md +29 -0
  147. package/skills/docs/pr-lifecycle-contract.md +209 -0
  148. package/skills/docs/public-dev-loop-contract.md +497 -0
  149. package/skills/docs/retrospective-checkpoint-contract.md +159 -0
  150. package/skills/docs/stop-conditions.md +29 -0
  151. package/skills/docs/structural-quality.md +42 -0
  152. package/skills/docs/tracker-first-loop-state.md +281 -0
  153. package/skills/docs/validation-policy.md +27 -0
  154. package/skills/docs/workflow-handoff-contract.md +135 -0
  155. package/skills/final-approval/SKILL.md +19 -0
  156. package/skills/local-implementation/SKILL.md +640 -0
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env node
2
+ import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
3
+ import { parsePositiveInteger, requireOptionValue, runChild } from "../_cli-primitives.mjs";
4
+ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
5
+ const USAGE = `Usage: manage-sub-issues.mjs <command> --repo <owner/name> --issue <number> [options]
6
+ Deterministic helper for reading, linking, ordering, and verifying GitHub sub-issue trees.
7
+ Commands:
8
+ list List sub-issues of a parent issue
9
+ add Add a child issue as a sub-issue of a parent
10
+ reorder Set the execution order of sub-issues
11
+ verify Verify the sub-issue tree state matches expectations
12
+ Common required options:
13
+ --repo <owner/name> Repository slug (e.g. owner/repo)
14
+ --issue <number> Parent issue number
15
+ add options:
16
+ --child <number> Child issue number to add as sub-issue
17
+ reorder options:
18
+ --order <n1,n2,...> Comma-separated issue numbers in the desired execution order (highest priority first)
19
+ verify options:
20
+ --expected <n1,n2,...> Comma-separated expected sub-issue numbers
21
+ --ordered Also verify that order matches exactly (optional)
22
+ Success output (stdout, JSON):
23
+ list:
24
+ { "ok": true, "repo": "owner/name", "issue": N, "command": "list",
25
+ "subIssues": [{ "number": M, "title": "...", "state": "open"|"closed", "id": ID }, ...] }
26
+ add:
27
+ { "ok": true, "repo": "owner/name", "issue": N, "command": "add", "child": M }
28
+ reorder:
29
+ { "ok": true, "repo": "owner/name", "issue": N, "command": "reorder", "order": [n1, n2, ...] }
30
+ verify:
31
+ { "ok": true, "repo": "owner/name", "issue": N, "command": "verify",
32
+ "verified": true|false, "expected": [...], "actual": [...],
33
+ "missing": [...], "unexpected": [...] }
34
+ When --ordered is set and sets match but order differs, also includes "orderMismatch": true
35
+ Error output (stderr, JSON):
36
+ Argument/usage errors:
37
+ { "ok": false, "error": "...", "usage": "..." }
38
+ gh/runtime failures:
39
+ { "ok": false, "error": "..." }`.trim();
40
+ const parseError = buildParseError(USAGE);
41
+ function parseIssueList(value) {
42
+ if (typeof value !== "string" || value.trim().length === 0) {
43
+ throw parseError("Issue list must be a non-empty comma-separated list of positive integers");
44
+ }
45
+ const parts = value.split(",").map((s) => s.trim());
46
+ if (parts.some((p) => !/^\d+$/.test(p) || Number(p) === 0)) {
47
+ throw parseError("Issue list must contain only positive integers");
48
+ }
49
+ const numbers = parts.map(Number);
50
+ const seen = new Set();
51
+ for (const n of numbers) {
52
+ if (seen.has(n)) {
53
+ throw parseError(`Duplicate issue number in list: ${n}`);
54
+ }
55
+ seen.add(n);
56
+ }
57
+ return numbers;
58
+ }
59
+ const VALID_COMMANDS = ["list", "add", "reorder", "verify"];
60
+ export function parseManageSubIssuesCliArgs(argv) {
61
+ const args = [...argv];
62
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
63
+ return { help: true };
64
+ }
65
+ const command = args.shift();
66
+ if (!VALID_COMMANDS.includes(command)) {
67
+ throw parseError(`Unknown command: ${command}. Valid commands: ${VALID_COMMANDS.join(", ")}`);
68
+ }
69
+ const options = {
70
+ help: false,
71
+ command,
72
+ repo: undefined,
73
+ issue: undefined,
74
+ child: undefined,
75
+ order: undefined,
76
+ expected: undefined,
77
+ ordered: false,
78
+ };
79
+ while (args.length > 0) {
80
+ const token = args.shift();
81
+ if (token === "--help" || token === "-h") {
82
+ options.help = true;
83
+ return options;
84
+ }
85
+ if (token === "--repo") {
86
+ options.repo = requireOptionValue(args, "--repo", parseError).trim();
87
+ continue;
88
+ }
89
+ if (token === "--issue") {
90
+ options.issue = parsePositiveInteger(requireOptionValue(args, "--issue", parseError), "Issue number", parseError);
91
+ continue;
92
+ }
93
+ if (token === "--child") {
94
+ options.child = parsePositiveInteger(requireOptionValue(args, "--child", parseError), "Issue number", parseError);
95
+ continue;
96
+ }
97
+ if (token === "--order") {
98
+ options.order = parseIssueList(requireOptionValue(args, "--order", parseError));
99
+ continue;
100
+ }
101
+ if (token === "--expected") {
102
+ options.expected = parseIssueList(requireOptionValue(args, "--expected", parseError));
103
+ continue;
104
+ }
105
+ if (token === "--ordered") {
106
+ options.ordered = true;
107
+ continue;
108
+ }
109
+ throw parseError(`Unknown argument: ${token}`);
110
+ }
111
+ if (options.repo === undefined || options.issue === undefined) {
112
+ throw parseError("Both --repo <owner/name> and --issue <number> are required");
113
+ }
114
+ try {
115
+ parseRepoSlug(options.repo);
116
+ } catch (error) {
117
+ throw parseError(error instanceof Error ? error.message : String(error));
118
+ }
119
+ if (command === "list") {
120
+ if (options.child !== undefined) {
121
+ throw parseError("The list command does not accept --child");
122
+ }
123
+ if (options.order !== undefined) {
124
+ throw parseError("The list command does not accept --order");
125
+ }
126
+ if (options.expected !== undefined) {
127
+ throw parseError("The list command does not accept --expected");
128
+ }
129
+ if (options.ordered) {
130
+ throw parseError("The list command does not accept --ordered");
131
+ }
132
+ }
133
+ if (command === "add") {
134
+ if (options.child === undefined) {
135
+ throw parseError("The add command requires --child <number>");
136
+ }
137
+ if (options.order !== undefined) {
138
+ throw parseError("The add command does not accept --order");
139
+ }
140
+ if (options.expected !== undefined) {
141
+ throw parseError("The add command does not accept --expected");
142
+ }
143
+ if (options.ordered) {
144
+ throw parseError("The add command does not accept --ordered");
145
+ }
146
+ }
147
+ if (command === "reorder") {
148
+ if (options.order === undefined) {
149
+ throw parseError("The reorder command requires --order <n1,n2,...>");
150
+ }
151
+ if (options.child !== undefined) {
152
+ throw parseError("The reorder command does not accept --child");
153
+ }
154
+ if (options.expected !== undefined) {
155
+ throw parseError("The reorder command does not accept --expected");
156
+ }
157
+ if (options.ordered) {
158
+ throw parseError("The reorder command does not accept --ordered");
159
+ }
160
+ }
161
+ if (command === "verify") {
162
+ if (options.expected === undefined) {
163
+ throw parseError("The verify command requires --expected <n1,n2,...>");
164
+ }
165
+ if (options.child !== undefined) {
166
+ throw parseError("The verify command does not accept --child");
167
+ }
168
+ if (options.order !== undefined) {
169
+ throw parseError("The verify command does not accept --order");
170
+ }
171
+ }
172
+ return options;
173
+ }
174
+ function buildSubIssuesListPath(owner, name, issue) {
175
+ return `repos/${owner}/${name}/issues/${issue}/sub_issues`;
176
+ }
177
+ function buildIssueGetPath(owner, name, issue) {
178
+ return `repos/${owner}/${name}/issues/${issue}`;
179
+ }
180
+ function buildSubIssueAddPath(owner, name, issue) {
181
+ return `repos/${owner}/${name}/issues/${issue}/sub_issues`;
182
+ }
183
+ function buildSubIssueReorderPath(owner, name, issue) {
184
+ return `repos/${owner}/${name}/issues/${issue}/sub_issues/priority`;
185
+ }
186
+ function normalizeSubIssue(raw) {
187
+ if (!raw || typeof raw !== "object") {
188
+ return null;
189
+ }
190
+ const id = raw.id;
191
+ const number = raw.number;
192
+ const title = typeof raw.title === "string" ? raw.title : "";
193
+ const state = typeof raw.state === "string" ? raw.state.toLowerCase() : null;
194
+ if (!Number.isInteger(id) || id <= 0) {
195
+ return null;
196
+ }
197
+ if (!Number.isInteger(number) || number <= 0) {
198
+ return null;
199
+ }
200
+ if (state !== "open" && state !== "closed") {
201
+ return null;
202
+ }
203
+ return { id, number, title, state };
204
+ }
205
+
206
+ /**
207
+ * Call gh api with optional JSON parsing.
208
+ * @param {boolean} [expectJson=true] - When false, skip JSON parsing (for empty-body responses like PATCH reorder).
209
+ */
210
+ async function ghApi(ghCommand, args, env, expectJson = true) {
211
+ const result = await runChild(ghCommand, ["api", ...args], env);
212
+ if (result.code !== 0) {
213
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
214
+ throw new Error(`gh api command failed: ${detail}`);
215
+ }
216
+ if (!expectJson) {
217
+ return null;
218
+ }
219
+ return parseJsonText(result.stdout);
220
+ }
221
+ async function listSubIssues(owner, name, issue, { env, ghCommand }) {
222
+ const path = buildSubIssuesListPath(owner, name, issue);
223
+ const payload = await ghApi(ghCommand, [path], env);
224
+ if (!Array.isArray(payload)) {
225
+ throw new Error("Invalid sub-issues payload: expected an array");
226
+ }
227
+ const subIssues = [];
228
+ for (const raw of payload) {
229
+ const normalized = normalizeSubIssue(raw);
230
+ if (normalized) {
231
+ subIssues.push(normalized);
232
+ }
233
+ }
234
+ return subIssues;
235
+ }
236
+ async function getIssueId(owner, name, issueNumber, { env, ghCommand }) {
237
+ const path = buildIssueGetPath(owner, name, issueNumber);
238
+ const payload = await ghApi(ghCommand, [path], env);
239
+ const id = payload?.id;
240
+ if (!Number.isInteger(id) || id <= 0) {
241
+ throw new Error(`Could not resolve id for issue #${issueNumber}`);
242
+ }
243
+ return id;
244
+ }
245
+ export async function runList({ repo, issue }, { env = process.env, ghCommand = "gh" } = {}) {
246
+ const { owner, name } = parseRepoSlug(repo);
247
+ const subIssues = await listSubIssues(owner, name, issue, { env, ghCommand });
248
+ return {
249
+ ok: true,
250
+ repo,
251
+ issue,
252
+ command: "list",
253
+ subIssues: subIssues.map(({ number, title, state, id }) => ({ number, title, state, id })),
254
+ };
255
+ }
256
+ export async function runAdd({ repo, issue, child }, { env = process.env, ghCommand = "gh" } = {}) {
257
+ const { owner, name } = parseRepoSlug(repo);
258
+ const childId = await getIssueId(owner, name, child, { env, ghCommand });
259
+ const path = buildSubIssueAddPath(owner, name, issue);
260
+ await ghApi(ghCommand, ["-X", "POST", path, "-F", `sub_issue_id=${childId}`], env, false);
261
+ return {
262
+ ok: true,
263
+ repo,
264
+ issue,
265
+ command: "add",
266
+ child,
267
+ };
268
+ }
269
+ export async function runReorder({ repo, issue, order }, { env = process.env, ghCommand = "gh" } = {}) {
270
+ const { owner, name } = parseRepoSlug(repo);
271
+ const subIssues = await listSubIssues(owner, name, issue, { env, ghCommand });
272
+ const idByNumber = new Map(subIssues.map((si) => [si.number, si.id]));
273
+ for (const n of order) {
274
+ if (!idByNumber.has(n)) {
275
+ throw new Error(`Issue #${n} is not a sub-issue of #${issue}`);
276
+ }
277
+ }
278
+ const reorderPath = buildSubIssueReorderPath(owner, name, issue);
279
+ let afterId = 0;
280
+ for (const n of order) {
281
+ const subIssueId = idByNumber.get(n);
282
+ const fieldArgs = ["-F", `sub_issue_id=${subIssueId}`, "-F", `after_id=${afterId}`];
283
+ await ghApi(ghCommand, ["-X", "PATCH", reorderPath, ...fieldArgs], env, false);
284
+ afterId = subIssueId;
285
+ }
286
+ return {
287
+ ok: true,
288
+ repo,
289
+ issue,
290
+ command: "reorder",
291
+ order,
292
+ };
293
+ }
294
+ export function computeVerifyResult({ repo, issue, expected, ordered, subIssues }) {
295
+ const actualNumbers = subIssues.map((si) => si.number);
296
+ const expectedCounts = new Map();
297
+ const actualCounts = new Map();
298
+ for (const number of expected) {
299
+ expectedCounts.set(number, (expectedCounts.get(number) ?? 0) + 1);
300
+ }
301
+ for (const number of actualNumbers) {
302
+ actualCounts.set(number, (actualCounts.get(number) ?? 0) + 1);
303
+ }
304
+ const allNumbers = new Set([...expectedCounts.keys(), ...actualCounts.keys()]);
305
+ const missing = [];
306
+ const unexpected = [];
307
+ for (const number of allNumbers) {
308
+ const expectedCount = expectedCounts.get(number) ?? 0;
309
+ const actualCount = actualCounts.get(number) ?? 0;
310
+ if (actualCount < expectedCount) {
311
+ missing.push(...Array(expectedCount - actualCount).fill(number));
312
+ }
313
+ if (actualCount > expectedCount) {
314
+ unexpected.push(...Array(actualCount - expectedCount).fill(number));
315
+ }
316
+ }
317
+ if (missing.length > 0 || unexpected.length > 0) {
318
+ return {
319
+ ok: true,
320
+ repo,
321
+ issue,
322
+ command: "verify",
323
+ verified: false,
324
+ expected,
325
+ actual: actualNumbers,
326
+ missing,
327
+ unexpected,
328
+ };
329
+ }
330
+ if (ordered) {
331
+ const orderMismatch = !expected.every((number, index) => actualNumbers[index] === number);
332
+ if (orderMismatch) {
333
+ return {
334
+ ok: true,
335
+ repo,
336
+ issue,
337
+ command: "verify",
338
+ verified: false,
339
+ expected,
340
+ actual: actualNumbers,
341
+ missing: [],
342
+ unexpected: [],
343
+ orderMismatch: true,
344
+ };
345
+ }
346
+ }
347
+ return {
348
+ ok: true,
349
+ repo,
350
+ issue,
351
+ command: "verify",
352
+ verified: true,
353
+ expected,
354
+ actual: actualNumbers,
355
+ missing: [],
356
+ unexpected: [],
357
+ };
358
+ }
359
+ export async function runVerify(
360
+ { repo, issue, expected, ordered },
361
+ { env = process.env, ghCommand = "gh" } = {},
362
+ ) {
363
+ const { owner, name } = parseRepoSlug(repo);
364
+ const subIssues = await listSubIssues(owner, name, issue, { env, ghCommand });
365
+ return computeVerifyResult({ repo, issue, expected, ordered, subIssues });
366
+ }
367
+ export async function runCli(
368
+ argv = process.argv.slice(2),
369
+ { stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
370
+ ) {
371
+ const options = parseManageSubIssuesCliArgs(argv);
372
+ if (options.help) {
373
+ stdout.write(`${USAGE}\n`);
374
+ return;
375
+ }
376
+ const { command, repo, issue, child, order, expected, ordered } = options;
377
+ let result;
378
+ if (command === "list") {
379
+ result = await runList({ repo, issue }, { env, ghCommand });
380
+ } else if (command === "add") {
381
+ result = await runAdd({ repo, issue, child }, { env, ghCommand });
382
+ } else if (command === "reorder") {
383
+ result = await runReorder({ repo, issue, order }, { env, ghCommand });
384
+ } else if (command === "verify") {
385
+ result = await runVerify({ repo, issue, expected, ordered }, { env, ghCommand });
386
+ }
387
+ stdout.write(`${JSON.stringify(result)}\n`);
388
+ }
389
+ if (isDirectCliRun(import.meta.url)) {
390
+ runCli().catch((error) => {
391
+ process.stderr.write(`${formatCliError(error)}\n`);
392
+ process.exitCode = 1;
393
+ });
394
+ }