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,518 @@
1
+ #!/usr/bin/env node
2
+ import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
3
+ import { runChild as _runChild } from "../_cli-primitives.mjs";
4
+
5
+ const USAGE = `Usage: dev-loops project reorder --repo <owner/name> --project <number|id> --item <number|node-id> [--after <number|node-id>]
6
+
7
+ Reorder a GitHub Projects V2 item by board position via updateProjectV2ItemPosition.
8
+ Moves item to top when no --after is provided, or after the specified item.
9
+
10
+ Options:
11
+ --repo <owner/name> Required. Repository to scope the project search.
12
+ --project <number|id> Required. Project number (integer) or node ID.
13
+ --item <number|node-id> Required. Item to reorder: issue/PR number, or project item node ID.
14
+ --after <number|node-id> Position after this item. When omitted, move to top.
15
+ --help, -h Show this help.
16
+
17
+ Output (stdout):
18
+ JSON: { ok: true, item: { itemId, issueNumber, prNumber, status, position }, after: { itemId, issueNumber, prNumber } | null }
19
+
20
+ Exit codes:
21
+ 0 — success
22
+ 1 — usage or argument error
23
+ 2 — GitHub API error
24
+ 3 — project, item, or after-item not found
25
+ `.trim();
26
+
27
+ const VALID_ARGS = new Set(["--repo", "--project", "--item", "--after", "--help", "-h"]);
28
+
29
+ function parseArgs(argv) {
30
+ const args = {};
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const arg = argv[i];
33
+ if (!VALID_ARGS.has(arg) && arg.startsWith("-")) {
34
+ throw Object.assign(
35
+ new Error(`Unknown flag: ${arg}`),
36
+ { code: "INVALID_ARGS", usage: USAGE },
37
+ );
38
+ }
39
+ if (arg === "--repo") {
40
+ if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
41
+ throw Object.assign(new Error("--repo requires a value (owner/name)"), { code: "INVALID_REPO" });
42
+ }
43
+ args.repo = argv[++i];
44
+ } else if (arg === "--project") {
45
+ if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
46
+ throw Object.assign(new Error("--project requires a value (number or node ID)"), { code: "INVALID_PROJECT" });
47
+ }
48
+ args.project = argv[++i];
49
+ } else if (arg === "--item") {
50
+ if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
51
+ throw Object.assign(new Error("--item requires a value (number or node ID)"), { code: "INVALID_ITEM" });
52
+ }
53
+ args.item = argv[++i];
54
+ } else if (arg === "--after") {
55
+ if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
56
+ throw Object.assign(new Error("--after requires a value (number or node ID)"), { code: "INVALID_AFTER" });
57
+ }
58
+ args.after = argv[++i];
59
+ } else if (arg === "--help" || arg === "-h") {
60
+ args.help = true;
61
+ } else {
62
+ throw Object.assign(new Error(`Unexpected argument: ${arg}`), { code: "INVALID_ARGS", usage: USAGE });
63
+ }
64
+ }
65
+ return args;
66
+ }
67
+
68
+ // ── Validation ───────────────────────────────────────────────────────────
69
+
70
+ const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
71
+ const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
72
+ const GLOBAL_NODE_ID_RE = /^[A-Za-z0-9_]+$/;
73
+
74
+ function validateRepo(repo) {
75
+ if (!repo || typeof repo !== "string") {
76
+ throw Object.assign(new Error("--repo is required"), { code: "INVALID_REPO" });
77
+ }
78
+ const trimmed = repo.trim();
79
+ if (trimmed !== repo) {
80
+ throw Object.assign(
81
+ new Error(`--repo must not have leading/trailing whitespace, got "${repo}"`),
82
+ { code: "INVALID_REPO" },
83
+ );
84
+ }
85
+ const slashIdx = repo.indexOf("/");
86
+ if (slashIdx === -1) {
87
+ throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
88
+ }
89
+ const owner = repo.slice(0, slashIdx);
90
+ const name = repo.slice(slashIdx + 1);
91
+ if (!owner || !name || !OWNER_RE.test(owner) || !REPO_NAME_RE.test(name)) {
92
+ throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
93
+ }
94
+ return repo;
95
+ }
96
+
97
+ function parseProjectRef(raw) {
98
+ if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
99
+ throw Object.assign(new Error("--project is required"), { code: "INVALID_PROJECT" });
100
+ }
101
+ const trimmed = raw.trim();
102
+ const asNum = Number(trimmed);
103
+ if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
104
+ return { kind: "number", value: asNum };
105
+ }
106
+ if (trimmed === "0") {
107
+ throw Object.assign(new Error(`--project must be a positive integer or a node ID, got "${raw}"`), { code: "INVALID_PROJECT" });
108
+ }
109
+ if (GLOBAL_NODE_ID_RE.test(trimmed)) {
110
+ return { kind: "id", value: trimmed };
111
+ }
112
+ throw Object.assign(new Error(`--project must be a positive integer or a node ID, got "${raw}"`), { code: "INVALID_PROJECT" });
113
+ }
114
+
115
+ function parseItemRef(raw) {
116
+ if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
117
+ throw Object.assign(new Error("--item is required"), { code: "INVALID_ITEM" });
118
+ }
119
+ const trimmed = raw.trim();
120
+ const asNum = Number(trimmed);
121
+ if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
122
+ return { kind: "number", value: asNum };
123
+ }
124
+ if (trimmed === "0") {
125
+ throw Object.assign(new Error(`--item must be a positive integer or an item node ID, got "${raw}"`), { code: "INVALID_ITEM" });
126
+ }
127
+ if (GLOBAL_NODE_ID_RE.test(trimmed)) {
128
+ return { kind: "id", value: trimmed };
129
+ }
130
+ throw Object.assign(new Error(`--item must be a positive integer or an item node ID, got "${raw}"`), { code: "INVALID_ITEM" });
131
+ }
132
+
133
+ // ── API helpers ──────────────────────────────────────────────────────────
134
+
135
+ async function ghGraphql(query, vars, env, runChild = _runChild) {
136
+ const fieldArgs = [];
137
+ for (const [key, value] of Object.entries(vars)) {
138
+ fieldArgs.push("--field", `${key}=${value}`);
139
+ }
140
+ const result = await runChild(
141
+ "gh",
142
+ ["api", "graphql", "--field", `query=${query}`, ...fieldArgs],
143
+ env,
144
+ );
145
+ if (result.code !== 0) {
146
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
147
+ throw Object.assign(new Error(`gh api graphql failed: ${detail}`), { code: "GH_API_ERROR" });
148
+ }
149
+ const payload = parseJsonText(result.stdout);
150
+ if (payload.errors && payload.errors.length > 0) {
151
+ throw Object.assign(
152
+ new Error(`GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`),
153
+ { code: "GRAPHQL_ERROR" },
154
+ );
155
+ }
156
+ return payload;
157
+ }
158
+
159
+ // ── GraphQL fragments ────────────────────────────────────────────────────
160
+
161
+ const GET_USER_ID = [
162
+ "query($login:String!) {",
163
+ " user(login:$login) { id }",
164
+ "}"
165
+ ].join("\n");
166
+
167
+ const GET_ORG_ID = [
168
+ "query($login:String!) {",
169
+ " organization(login:$login) { id }",
170
+ "}"
171
+ ].join("\n");
172
+
173
+ const LIST_USER_PROJECTS = [
174
+ "query($login:String!, $after:String) {",
175
+ " user(login:$login) {",
176
+ " projectsV2(first:50, after:$after) {",
177
+ " pageInfo { hasNextPage endCursor }",
178
+ " nodes { id number title url }",
179
+ " }",
180
+ " }",
181
+ "}"
182
+ ].join("\n");
183
+
184
+ const LIST_ORG_PROJECTS = [
185
+ "query($login:String!, $after:String) {",
186
+ " organization(login:$login) {",
187
+ " projectsV2(first:50, after:$after) {",
188
+ " pageInfo { hasNextPage endCursor }",
189
+ " nodes { id number title url }",
190
+ " }",
191
+ " }",
192
+ "}"
193
+ ].join("\n");
194
+
195
+ const GET_PROJECT_ITEMS_BY_CONTENT = [
196
+ "query($projectId:ID!, $after:String) {",
197
+ " node(id:$projectId) {",
198
+ " ... on ProjectV2 {",
199
+ " items(first:10, after:$after, orderBy:{field:POSITION, direction:ASC}) {",
200
+ " pageInfo { hasNextPage endCursor }",
201
+ " nodes {",
202
+ " id",
203
+ " fieldValues(first:20) {",
204
+ " nodes {",
205
+ " ... on ProjectV2ItemFieldSingleSelectValue {",
206
+ " field { ... on ProjectV2SingleSelectField { id name } }",
207
+ " name",
208
+ " }",
209
+ " }",
210
+ " }",
211
+ " content {",
212
+ " ... on Issue { __typename number repository { nameWithOwner } }",
213
+ " ... on PullRequest { __typename number repository { nameWithOwner } }",
214
+ " }",
215
+ " }",
216
+ " }",
217
+ " }",
218
+ " }",
219
+ "}"
220
+ ].join("\n");
221
+
222
+ const GET_PROJECT_ITEM = [
223
+ "query($projectId:ID!, $itemId:ID!) {",
224
+ " node(id:$projectId) {",
225
+ " ... on ProjectV2 {",
226
+ " item: item(id:$itemId) {",
227
+ " id",
228
+ " fieldValues(first:20) {",
229
+ " nodes {",
230
+ " ... on ProjectV2ItemFieldSingleSelectValue {",
231
+ " field { ... on ProjectV2SingleSelectField { id name } }",
232
+ " name",
233
+ " }",
234
+ " }",
235
+ " }",
236
+ " content {",
237
+ " ... on Issue { __typename number title url }",
238
+ " ... on PullRequest { __typename number title url }",
239
+ " }",
240
+ " }",
241
+ " }",
242
+ " }",
243
+ "}"
244
+ ].join("\n");
245
+
246
+ const UPDATE_ITEM_POSITION = [
247
+ "mutation($projectId:ID!, $itemId:ID!, $afterId:ID) {",
248
+ " updateProjectV2ItemPosition(input:{projectId:$projectId, itemId:$itemId, afterId:$afterId}) {",
249
+ " clientMutationId",
250
+ " }",
251
+ "}"
252
+ ].join("\n");
253
+
254
+ // ── Owner resolution ────────────────────────────────────────────────────
255
+
256
+ async function resolveOwner(login, env, runChild) {
257
+ const userPayload = await ghGraphql(GET_USER_ID, { login }, env, runChild);
258
+ if (userPayload?.data?.user?.id) {
259
+ return { id: userPayload.data.user.id, kind: "user" };
260
+ }
261
+ const orgPayload = await ghGraphql(GET_ORG_ID, { login }, env, runChild);
262
+ if (orgPayload?.data?.organization?.id) {
263
+ return { id: orgPayload.data.organization.id, kind: "org" };
264
+ }
265
+ throw Object.assign(
266
+ new Error(`Could not resolve owner ID for "${login}"`),
267
+ { code: "NO_USER_ID" },
268
+ );
269
+ }
270
+
271
+ // ── Paginated project listing ────────────────────────────────────────────
272
+
273
+ async function listAllProjects(login, kind, env, runChild) {
274
+ const query = kind === "org" ? LIST_ORG_PROJECTS : LIST_USER_PROJECTS;
275
+ const projects = [];
276
+ let after = null;
277
+ while (true) {
278
+ const vars = { login };
279
+ if (after) vars.after = after;
280
+ const payload = await ghGraphql(query, vars, env, runChild);
281
+ const connection = kind === "org"
282
+ ? payload?.data?.organization?.projectsV2
283
+ : payload?.data?.user?.projectsV2;
284
+ const nodes = connection?.nodes ?? [];
285
+ projects.push(...nodes);
286
+ const pageInfo = connection?.pageInfo ?? {};
287
+ if (!pageInfo.hasNextPage) break;
288
+ if (!pageInfo.endCursor) {
289
+ throw Object.assign(
290
+ new Error("Invalid projects list payload: hasNextPage is true but endCursor is missing"),
291
+ { code: "GH_API_ERROR" },
292
+ );
293
+ }
294
+ after = pageInfo.endCursor;
295
+ }
296
+ return projects;
297
+ }
298
+
299
+ // ── Resolve an item in a project by reference (number or node ID) ──────
300
+
301
+ async function resolveProjectItem(projectId, itemRef, owner, repoName, repo, env, runChild) {
302
+ let itemId;
303
+ let issueNumber = null;
304
+ let prNumber = null;
305
+ let status = null;
306
+
307
+ if (itemRef.kind === "id") {
308
+ // Direct item node ID lookup
309
+ const itemPayload = await ghGraphql(GET_PROJECT_ITEM, {
310
+ projectId,
311
+ itemId: itemRef.value,
312
+ }, env, runChild);
313
+ const item = itemPayload?.data?.node?.item;
314
+ if (!item) {
315
+ throw Object.assign(
316
+ new Error(`Item "${itemRef.value}" not found in project`),
317
+ { code: "ITEM_NOT_FOUND" },
318
+ );
319
+ }
320
+ itemId = item.id;
321
+ const fvs = item.fieldValues?.nodes ?? [];
322
+ for (const fv of fvs) {
323
+ if (fv && fv.field && fv.field.name === "Status") {
324
+ status = fv.name;
325
+ break;
326
+ }
327
+ }
328
+ if (item.content) {
329
+ if (item.content.__typename === "Issue") {
330
+ issueNumber = item.content.number;
331
+ } else {
332
+ prNumber = item.content.number;
333
+ }
334
+ }
335
+ } else {
336
+ // Look up by issue/PR number in the project (paginated)
337
+ const targetNumber = itemRef.value;
338
+ let allItems = [];
339
+ let after = null;
340
+ while (true) {
341
+ const vars = { projectId };
342
+ if (after) vars.after = after;
343
+ const itemsPayload = await ghGraphql(GET_PROJECT_ITEMS_BY_CONTENT, vars, env, runChild);
344
+ const connection = itemsPayload?.data?.node?.items;
345
+ const nodes = connection?.nodes ?? [];
346
+ allItems.push(...nodes);
347
+ const pageInfo = connection?.pageInfo ?? {};
348
+ if (!pageInfo.hasNextPage) break;
349
+ if (!pageInfo.endCursor) {
350
+ throw Object.assign(
351
+ new Error("Invalid items payload: hasNextPage is true but endCursor is missing"),
352
+ { code: "GH_API_ERROR" },
353
+ );
354
+ }
355
+ after = pageInfo.endCursor;
356
+ }
357
+
358
+ // Filter by matching repo AND number exactly
359
+ const matchingItems = allItems.filter((it) => {
360
+ if (!it.content) return false;
361
+ if (it.content.repository?.nameWithOwner !== repo) return false;
362
+ return it.content.number === targetNumber;
363
+ });
364
+
365
+ if (matchingItems.length === 0) {
366
+ throw Object.assign(
367
+ new Error(`Item #${targetNumber} not found in project for repo "${repo}"`),
368
+ { code: "ITEM_NOT_FOUND" },
369
+ );
370
+ }
371
+
372
+ // Use the first match (by position order)
373
+ const match = matchingItems[0];
374
+ itemId = match.id;
375
+ const fvs = match.fieldValues?.nodes ?? [];
376
+ for (const fv of fvs) {
377
+ if (fv && fv.field && fv.field.name === "Status") {
378
+ status = fv.name;
379
+ break;
380
+ }
381
+ }
382
+ if (match.content) {
383
+ if (match.content.__typename === "Issue") {
384
+ issueNumber = match.content.number;
385
+ } else {
386
+ prNumber = match.content.number;
387
+ }
388
+ }
389
+ }
390
+
391
+ return { itemId, issueNumber, prNumber, status };
392
+ }
393
+
394
+ // ── Exit code classification ────────────────────────────────────────────
395
+
396
+ function classifyExitCode(err) {
397
+ if (err.code === "INVALID_REPO" || err.code === "INVALID_PROJECT" || err.code === "INVALID_ITEM" ||
398
+ err.code === "INVALID_AFTER" || err.code === "INVALID_ARGS") return 1;
399
+ if (err.code === "PROJECT_NOT_FOUND" || err.code === "ITEM_NOT_FOUND" || err.code === "AFTER_ITEM_NOT_FOUND") return 3;
400
+ return 2;
401
+ }
402
+
403
+ // ── Main logic ──────────────────────────────────────────────────────────
404
+
405
+ async function main(args, { env = process.env, runChild } = {}) {
406
+ const child = runChild ?? _runChild;
407
+ const repo = validateRepo(args.repo);
408
+ const [owner, repoName] = repo.split("/");
409
+ const projectRef = parseProjectRef(args.project);
410
+ const itemRef = parseItemRef(args.item);
411
+
412
+ // --after is optional
413
+ let afterRef = null;
414
+ if (args.after !== undefined) {
415
+ afterRef = parseItemRef(args.after);
416
+ }
417
+
418
+ // 1. Resolve owner
419
+ const { id: ownerId, kind: ownerKind } = await resolveOwner(owner, env, child);
420
+
421
+ // 2. Resolve project
422
+ const projects = await listAllProjects(owner, ownerKind, env, child);
423
+ let project;
424
+ if (projectRef.kind === "id") {
425
+ project = projects.find((p) => p.id === projectRef.value);
426
+ } else {
427
+ project = projects.find((p) => p.number === projectRef.value);
428
+ }
429
+ if (!project) {
430
+ throw Object.assign(
431
+ new Error(`Project ${projectRef.kind === "id" ? `"${projectRef.value}"` : `number ${projectRef.value}`} not found under owner "${owner}"`),
432
+ { code: "PROJECT_NOT_FOUND" },
433
+ );
434
+ }
435
+
436
+ // 3. Resolve the item to move
437
+ const item = await resolveProjectItem(project.id, itemRef, owner, repoName, repo, env, child);
438
+
439
+ // 4. Resolve the after-item (if provided)
440
+ let afterItem = null;
441
+ if (afterRef) {
442
+ afterItem = await resolveProjectItem(project.id, afterRef, owner, repoName, repo, env, child);
443
+
444
+ // Fail closed: cannot reorder after itself
445
+ if (afterItem.itemId === item.itemId) {
446
+ throw Object.assign(
447
+ new Error("Cannot reorder an item after itself"),
448
+ { code: "INVALID_AFTER" },
449
+ );
450
+ }
451
+ }
452
+
453
+ // 5. Execute reorder mutation
454
+ const mutationVars = {
455
+ projectId: project.id,
456
+ itemId: item.itemId,
457
+ afterId: afterItem ? afterItem.itemId : null,
458
+ };
459
+
460
+ const mutationPayload = await ghGraphql(UPDATE_ITEM_POSITION, mutationVars, env, child);
461
+
462
+ if (!mutationPayload?.data?.updateProjectV2ItemPosition) {
463
+ throw Object.assign(new Error("Failed to reorder item"), { code: "MUTATION_FAILED" });
464
+ }
465
+
466
+ const result = {
467
+ ok: true,
468
+ item: {
469
+ itemId: item.itemId,
470
+ issueNumber: item.issueNumber,
471
+ prNumber: item.prNumber,
472
+ status: item.status,
473
+ position: afterItem ? "after" : "top",
474
+ },
475
+ after: afterItem
476
+ ? {
477
+ itemId: afterItem.itemId,
478
+ issueNumber: afterItem.issueNumber,
479
+ prNumber: afterItem.prNumber,
480
+ }
481
+ : null,
482
+ };
483
+
484
+ return result;
485
+ }
486
+
487
+ // ── CLI entrypoint ──────────────────────────────────────────────────────
488
+
489
+ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
490
+ let args;
491
+ try {
492
+ args = parseArgs(argv);
493
+ } catch (err) {
494
+ stderr.write(`${formatCliError(err)}\n`);
495
+ process.exitCode = 1;
496
+ return;
497
+ }
498
+ if (args.help) {
499
+ stdout.write(USAGE);
500
+ return;
501
+ }
502
+ try {
503
+ const result = await main(args, { env });
504
+ stdout.write(JSON.stringify(result) + "\n");
505
+ } catch (err) {
506
+ stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
507
+ process.exitCode = classifyExitCode(err);
508
+ }
509
+ }
510
+
511
+ if (isDirectCliRun(import.meta.url)) {
512
+ runCli(process.argv.slice(2)).catch((error) => {
513
+ process.stderr.write(JSON.stringify({ ok: false, error: error.message, code: error.code ?? "UNKNOWN" }) + "\n");
514
+ process.exitCode = 2;
515
+ });
516
+ }
517
+
518
+ export { main };