dev-loops 0.5.0 → 0.7.1

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 (177) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +2 -1
  3. package/.claude/agents/review.md +2 -1
  4. package/.claude/commands/loop-auto.md +7 -0
  5. package/.claude/commands/loop-continue.md +15 -0
  6. package/.claude/commands/loop-enqueue.md +22 -0
  7. package/.claude/commands/loop-grill.md +17 -0
  8. package/.claude/commands/loop-info.md +7 -0
  9. package/.claude/commands/loop-queue-status.md +24 -0
  10. package/.claude/commands/loop-start-spike.md +16 -0
  11. package/.claude/commands/loop-start.md +7 -0
  12. package/.claude/commands/loop-status.md +6 -0
  13. package/.claude/hooks/_bash-command-classify.mjs +333 -29
  14. package/.claude/hooks/_hook-decisions.mjs +138 -15
  15. package/.claude/hooks/_run-context.mjs +11 -4
  16. package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
  17. package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
  18. package/.claude/skills/dev-loop/SKILL.md +19 -10
  19. package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
  20. package/.claude/skills/docs/anti-patterns.md +2 -1
  21. package/.claude/skills/docs/artifact-authority-contract.md +22 -4
  22. package/.claude/skills/docs/copilot-loop-operations.md +1 -1
  23. package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
  24. package/.claude/skills/docs/issue-intake-procedure.md +1 -0
  25. package/.claude/skills/docs/local-planning-flow.md +1 -1
  26. package/.claude/skills/docs/local-planning-worked-example.md +1 -1
  27. package/.claude/skills/docs/merge-preconditions.md +17 -1
  28. package/.claude/skills/docs/plan-file-contract.md +1 -1
  29. package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
  30. package/.claude/skills/docs/release-runbook.md +45 -0
  31. package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
  32. package/.claude/skills/docs/ui-e2e-scoping-step.md +134 -0
  33. package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
  34. package/.claude/skills/local-implementation/SKILL.md +26 -15
  35. package/.claude/skills/loop-grill/SKILL.md +163 -0
  36. package/AGENTS.md +1 -0
  37. package/CHANGELOG.md +125 -0
  38. package/README.md +29 -10
  39. package/agents/dev-loop.agent.md +11 -3
  40. package/agents/developer.agent.md +1 -1
  41. package/agents/docs.agent.md +1 -1
  42. package/agents/fixer.agent.md +1 -1
  43. package/agents/quality.agent.md +1 -1
  44. package/agents/refiner.agent.md +1 -1
  45. package/agents/review.agent.md +3 -2
  46. package/cli/index.mjs +35 -40
  47. package/extension/README.md +4 -4
  48. package/extension/checks.ts +1 -5
  49. package/extension/harness-types.ts +1 -0
  50. package/extension/index.ts +16 -1
  51. package/extension/pi-extension-adapter.ts +2 -0
  52. package/extension/presentation.ts +16 -13
  53. package/lib/dev-loops-core.mjs +141 -0
  54. package/package.json +10 -8
  55. package/scripts/claude/generate-claude-assets.mjs +15 -1
  56. package/scripts/github/capture-review-threads.mjs +2 -9
  57. package/scripts/github/comment-issue.mjs +174 -0
  58. package/scripts/github/create-label.mjs +133 -0
  59. package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
  60. package/scripts/github/detect-linked-issue-pr.mjs +22 -6
  61. package/scripts/github/edit-pr.mjs +259 -0
  62. package/scripts/github/fetch-ci-logs.mjs +208 -0
  63. package/scripts/github/list-issues.mjs +184 -0
  64. package/scripts/github/manage-sub-issues.mjs +46 -10
  65. package/scripts/github/offer-human-handoff.mjs +8 -5
  66. package/scripts/github/post-gate-findings.mjs +14 -2
  67. package/scripts/github/probe-ci-status.mjs +8 -2
  68. package/scripts/github/probe-copilot-review.mjs +21 -14
  69. package/scripts/github/ready-for-review.mjs +26 -8
  70. package/scripts/github/reconcile-draft-gate.mjs +7 -3
  71. package/scripts/github/reply-resolve-review-thread.mjs +16 -5
  72. package/scripts/github/reply-resolve-review-threads.mjs +69 -7
  73. package/scripts/github/request-copilot-review.mjs +8 -2
  74. package/scripts/github/resolve-handoff-candidates.mjs +7 -3
  75. package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
  76. package/scripts/github/stage-reviewer-draft.mjs +10 -2
  77. package/scripts/github/tick-verified-checkboxes.mjs +202 -0
  78. package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
  79. package/scripts/github/verify-fresh-review-context.mjs +150 -31
  80. package/scripts/github/view-pr.mjs +150 -0
  81. package/scripts/github/write-gate-context.mjs +248 -61
  82. package/scripts/github/write-gate-findings-log.mjs +75 -1
  83. package/scripts/lib/jq-output.mjs +18 -0
  84. package/scripts/loop/_handoff-contract.mjs +1 -0
  85. package/scripts/loop/_post-convergence-change.mjs +211 -0
  86. package/scripts/loop/_pr-runner-coordination.mjs +70 -0
  87. package/scripts/loop/_repo-root-resolver.mjs +47 -0
  88. package/scripts/loop/build-handoff-envelope.mjs +14 -4
  89. package/scripts/loop/check-retro-tooling.mjs +14 -3
  90. package/scripts/loop/checkpoint-contract.mjs +7 -4
  91. package/scripts/loop/cleanup-worktree.mjs +12 -3
  92. package/scripts/loop/conductor-monitor.mjs +12 -18
  93. package/scripts/loop/copilot-pr-handoff.mjs +135 -14
  94. package/scripts/loop/debt-remediate.mjs +24 -12
  95. package/scripts/loop/detect-change-scope.mjs +36 -7
  96. package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
  97. package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
  98. package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
  99. package/scripts/loop/detect-internal-only-pr.mjs +8 -2
  100. package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
  101. package/scripts/loop/detect-pr-gate-coordination-state.mjs +179 -66
  102. package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
  103. package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
  104. package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
  105. package/scripts/loop/ensure-worktree.mjs +8 -3
  106. package/scripts/loop/info.mjs +12 -4
  107. package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
  108. package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
  109. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
  110. package/scripts/loop/inspect-run-viewer.mjs +67 -4
  111. package/scripts/loop/inspect-run.mjs +8 -2
  112. package/scripts/loop/outer-loop.mjs +8 -4
  113. package/scripts/loop/pr-runner-coordination.mjs +2 -9
  114. package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
  115. package/scripts/loop/pre-flight-gate.mjs +9 -9
  116. package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
  117. package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
  118. package/scripts/loop/provision-worktree.mjs +74 -3
  119. package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
  120. package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
  121. package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
  122. package/scripts/loop/run-conductor-cycle.mjs +13 -2
  123. package/scripts/loop/run-queue.mjs +18 -9
  124. package/scripts/loop/run-refinement-audit.mjs +8 -9
  125. package/scripts/loop/run-watch-cycle.mjs +2 -9
  126. package/scripts/loop/sanctioned-commands.mjs +104 -0
  127. package/scripts/loop/steer-loop.mjs +29 -16
  128. package/scripts/loop/validate-pr-body-spec.mjs +207 -0
  129. package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
  130. package/scripts/pages/build-site.mjs +59 -8
  131. package/scripts/pages/build-state-atlas.mjs +441 -0
  132. package/scripts/projects/_resolve-project.mjs +148 -0
  133. package/scripts/projects/add-queue-item.mjs +60 -54
  134. package/scripts/projects/archive-done-items.mjs +49 -84
  135. package/scripts/projects/ensure-queue-board.mjs +10 -36
  136. package/scripts/projects/list-queue-items.mjs +116 -65
  137. package/scripts/projects/move-queue-item.mjs +28 -49
  138. package/scripts/projects/reconcile-queue.mjs +253 -0
  139. package/scripts/projects/reorder-queue-item.mjs +41 -47
  140. package/scripts/projects/resolve-active-board-item.mjs +255 -0
  141. package/scripts/projects/sync-item-status.mjs +15 -10
  142. package/scripts/refine/_refine-helpers.mjs +21 -5
  143. package/scripts/refine/exit-spike.mjs +18 -8
  144. package/scripts/refine/promote-plan.mjs +22 -16
  145. package/scripts/refine/prose-linkage-detector.mjs +1 -1
  146. package/scripts/refine/refine-plan-file.mjs +13 -4
  147. package/scripts/refine/refinement-completeness-checker.mjs +1 -1
  148. package/scripts/refine/scaffold-spike-file.mjs +179 -0
  149. package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
  150. package/scripts/refine/tree-integrity-validator.mjs +1 -1
  151. package/scripts/refine/validate-plan-file.mjs +1 -1
  152. package/scripts/refine/validate-spike-file.mjs +1 -1
  153. package/scripts/refine/verify.mjs +11 -6
  154. package/scripts/release/assert-core-dependency-version.mjs +123 -0
  155. package/scripts/release/extract-changelog-section.mjs +125 -0
  156. package/skills/copilot-pr-followup/SKILL.md +24 -12
  157. package/skills/dev-loop/SKILL.md +22 -6
  158. package/skills/docs/acceptance-criteria-verification.md +6 -2
  159. package/skills/docs/anti-patterns.md +2 -1
  160. package/skills/docs/artifact-authority-contract.md +22 -4
  161. package/skills/docs/copilot-loop-operations.md +1 -1
  162. package/skills/docs/cross-harness-regression-contract.md +60 -0
  163. package/skills/docs/issue-intake-procedure.md +1 -0
  164. package/skills/docs/local-planning-flow.md +1 -1
  165. package/skills/docs/local-planning-worked-example.md +1 -1
  166. package/skills/docs/merge-preconditions.md +17 -1
  167. package/skills/docs/plan-file-contract.md +1 -1
  168. package/skills/docs/public-dev-loop-contract.md +1 -1
  169. package/skills/docs/release-runbook.md +45 -0
  170. package/skills/docs/retrospective-checkpoint-contract.md +90 -76
  171. package/skills/docs/ui-e2e-scoping-step.md +134 -0
  172. package/skills/docs/workflow-handoff-contract.md +39 -0
  173. package/skills/local-implementation/SKILL.md +26 -15
  174. package/skills/loop-grill/SKILL.md +165 -0
  175. package/scripts/loop/conductor.mjs +0 -233
  176. package/scripts/loop/detect-stale-runner.mjs +0 -265
  177. package/scripts/loop/pre-push-main-guard.mjs +0 -117
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
3
+ import { requireTokenValue, runChild } from "../_cli-primitives.mjs";
4
+ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
5
+ import { parseArgs } from "node:util";
6
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
7
+
8
+ const STATES = new Set(["open", "closed", "all"]);
9
+
10
+ const USAGE = `Usage: list-issues.mjs --repo <owner/name> [--state <open|closed|all>] [--label <l>] [--limit <n>]
11
+ List/filter repository issues. Thin wrapper over \`gh issue list\` — use this instead
12
+ of an agent-level raw \`gh issue list\` so the loop's internal-tooling record stays
13
+ clean (#993). The queue tool lists the project board, not arbitrary issue queries;
14
+ this fills that gap (siblings: comment-issue.mjs, fetch-ci-logs.mjs).
15
+ Required:
16
+ --repo <owner/name> Repository slug (e.g. owner/repo)
17
+ Optional:
18
+ --state <open|closed|all> Issue state filter (default open)
19
+ --label <l> Filter by label (repeatable; AND-combined by gh)
20
+ --limit <n> Return at most <n> issues (default 30)
21
+ Output (stdout, JSON):
22
+ { "ok": true, "issues": [{ "number": 17, "title": "...", "state": "open", "labels": ["bug"] }, ...] }
23
+ Error output (stderr, JSON):
24
+ { "ok": false, "error": "...", "usage"?: "..." }
25
+ ${JQ_OUTPUT_USAGE}
26
+ Exit codes:
27
+ 0 Success
28
+ 1 Argument error or gh failure
29
+ 2 Invalid --jq filter`.trim();
30
+ const parseError = buildParseError(USAGE);
31
+
32
+ export function parseListIssuesCliArgs(argv) {
33
+ const { tokens } = parseArgs({
34
+ args: [...argv],
35
+ options: {
36
+ help: { type: "boolean", short: "h" },
37
+ repo: { type: "string" },
38
+ state: { type: "string" },
39
+ label: { type: "string", multiple: true },
40
+ limit: { type: "string" },
41
+ ...JQ_OUTPUT_PARSE_OPTIONS,
42
+ },
43
+ allowPositionals: true,
44
+ strict: false,
45
+ tokens: true,
46
+ });
47
+ const options = {
48
+ help: false,
49
+ repo: undefined,
50
+ state: "open",
51
+ labels: [],
52
+ limit: 30,
53
+ jq: undefined,
54
+ silent: false,
55
+ };
56
+ for (const token of tokens) {
57
+ if (token.kind === "positional") {
58
+ throw parseError(`Unknown argument: ${token.value}`);
59
+ }
60
+ if (token.kind !== "option") {
61
+ continue;
62
+ }
63
+ if (token.name === "help") {
64
+ options.help = true;
65
+ return options;
66
+ }
67
+ if (token.name === "repo") {
68
+ options.repo = requireTokenValue(token, parseError).trim();
69
+ continue;
70
+ }
71
+ if (token.name === "state") {
72
+ const state = requireTokenValue(token, parseError).trim().toLowerCase();
73
+ if (!STATES.has(state)) {
74
+ throw parseError("--state must be one of: open, closed, all");
75
+ }
76
+ options.state = state;
77
+ continue;
78
+ }
79
+ if (token.name === "label") {
80
+ const label = requireTokenValue(token, parseError).trim();
81
+ if (label.length === 0) {
82
+ throw parseError("--label must be a non-empty string");
83
+ }
84
+ options.labels.push(label);
85
+ continue;
86
+ }
87
+ if (token.name === "limit") {
88
+ const raw = requireTokenValue(token, parseError);
89
+ const value = Number(raw);
90
+ if (!Number.isInteger(value) || value < 1) {
91
+ throw parseError(`--limit must be a positive integer, got "${raw}"`);
92
+ }
93
+ options.limit = value;
94
+ continue;
95
+ }
96
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
97
+ throw parseError(`Unknown argument: ${token.rawName}`);
98
+ }
99
+ if (options.repo === undefined) {
100
+ throw parseError("Listing issues requires --repo <owner/name>");
101
+ }
102
+ try {
103
+ parseRepoSlug(options.repo);
104
+ } catch (error) {
105
+ throw parseError(error instanceof Error ? error.message : String(error));
106
+ }
107
+ return options;
108
+ }
109
+
110
+ // Returns a well-typed issue, or null if the gh entry is missing/invalid in any
111
+ // required field. Callers filter the nulls so the emitted shape stays
112
+ // well-typed (documented output) and --jq filters never hit null number/title/state.
113
+ function normalizeIssue(raw) {
114
+ if (!Number.isInteger(raw?.number) || typeof raw?.title !== "string" || typeof raw?.state !== "string") {
115
+ return null;
116
+ }
117
+ return {
118
+ number: raw.number,
119
+ title: raw.title,
120
+ // gh reports issue state UPPERCASE (OPEN/CLOSED); normalize to lowercase to
121
+ // match the --state flag vocabulary.
122
+ state: raw.state.toLowerCase(),
123
+ labels: Array.isArray(raw?.labels)
124
+ ? raw.labels.map((l) => (typeof l?.name === "string" ? l.name : null)).filter((n) => n !== null)
125
+ : [],
126
+ };
127
+ }
128
+
129
+ export async function listIssues(options, { env = process.env, ghCommand = "gh", run = runChild } = {}) {
130
+ const args = [
131
+ "issue",
132
+ "list",
133
+ "--repo",
134
+ options.repo,
135
+ "--state",
136
+ options.state,
137
+ "--limit",
138
+ String(options.limit),
139
+ "--json",
140
+ "number,title,state,labels",
141
+ ];
142
+ for (const label of options.labels) {
143
+ args.push("--label", label);
144
+ }
145
+ const result = await run(ghCommand, args, env);
146
+ if (result.code !== 0) {
147
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
148
+ throw new Error(`gh issue list failed: ${detail}`);
149
+ }
150
+ const payload = parseJsonText(result.stdout, { label: "gh issue list" });
151
+ if (!Array.isArray(payload)) {
152
+ throw new Error("gh issue list did not return a JSON array");
153
+ }
154
+ return { ok: true, issues: payload.map(normalizeIssue).filter((issue) => issue !== null) };
155
+ }
156
+
157
+ export async function runCli(
158
+ argv = process.argv.slice(2),
159
+ { stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh", run = runChild } = {},
160
+ ) {
161
+ let options;
162
+ try {
163
+ options = parseListIssuesCliArgs(argv);
164
+ } catch (error) {
165
+ stderr.write(`${formatCliError(error)}\n`);
166
+ return 1;
167
+ }
168
+ if (options.help) {
169
+ stdout.write(`${USAGE}\n`);
170
+ return 0;
171
+ }
172
+ let result;
173
+ try {
174
+ result = await listIssues(options, { env, ghCommand, run });
175
+ } catch (error) {
176
+ stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
177
+ return 1;
178
+ }
179
+ return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
180
+ }
181
+
182
+ if (isDirectCliRun(import.meta.url)) {
183
+ runCli().then((code) => { process.exitCode = code; });
184
+ }
@@ -3,6 +3,7 @@ import { parseArgs } from "node:util";
3
3
  import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
4
4
  import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
5
5
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
6
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
6
7
  const USAGE = `Usage: manage-sub-issues.mjs <command> --repo <owner/name> --issue <number> [options]
7
8
  Deterministic helper for reading, linking, ordering, and verifying GitHub sub-issue trees.
8
9
  Commands:
@@ -37,7 +38,8 @@ Error output (stderr, JSON):
37
38
  Argument/usage errors:
38
39
  { "ok": false, "error": "...", "usage": "..." }
39
40
  gh/runtime failures:
40
- { "ok": false, "error": "..." }`.trim();
41
+ { "ok": false, "error": "..." }
42
+ ${JQ_OUTPUT_USAGE}`.trim();
41
43
  const parseError = buildParseError(USAGE);
42
44
  function parseIssueList(value) {
43
45
  if (typeof value !== "string" || value.trim().length === 0) {
@@ -87,6 +89,7 @@ export function parseManageSubIssuesCliArgs(argv) {
87
89
  order: { type: "string" },
88
90
  expected: { type: "string" },
89
91
  ordered: { type: "boolean" },
92
+ ...JQ_OUTPUT_PARSE_OPTIONS,
90
93
  },
91
94
  allowPositionals: true,
92
95
  strict: false,
@@ -127,6 +130,7 @@ export function parseManageSubIssuesCliArgs(argv) {
127
130
  options.ordered = true;
128
131
  continue;
129
132
  }
133
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
130
134
  throw parseError(`Unknown argument: ${token.rawName}`);
131
135
  }
132
136
  if (options.repo === undefined || options.issue === undefined) {
@@ -224,15 +228,33 @@ function normalizeSubIssue(raw) {
224
228
  return { id, number, title, state };
225
229
  }
226
230
 
231
+ function extractEndpoint(args) {
232
+ return args.find((a) => typeof a === "string" && a.startsWith("repos/")) ?? "unknown endpoint";
233
+ }
234
+ function extractHttpStatus(output) {
235
+ const match = /^HTTP\/\S+\s+(\d{3})/m.exec(output ?? "");
236
+ return match ? match[1] : null;
237
+ }
227
238
  /**
228
239
  * Call gh api with optional JSON parsing.
229
240
  * @param {boolean} [expectJson=true] - When false, skip JSON parsing (for empty-body responses like PATCH reorder).
230
241
  */
231
242
  async function ghApi(ghCommand, args, env, expectJson = true) {
232
- const result = await runChild(ghCommand, ["api", ...args], env);
243
+ // Non-JSON calls (POST/PATCH with empty bodies) pass -i so a non-2xx response
244
+ // still surfaces its HTTP status even when the body is empty. JSON calls are
245
+ // left untouched since -i would break body parsing.
246
+ const captureStatus = !expectJson;
247
+ const apiArgs = captureStatus ? ["-i", ...args] : args;
248
+ const result = await runChild(ghCommand, ["api", ...apiArgs], env);
233
249
  if (result.code !== 0) {
234
- const detail = result.stderr.trim() || `exit code ${result.code}`;
235
- throw new Error(`gh api command failed: ${detail}`);
250
+ const endpoint = extractEndpoint(args);
251
+ const status = captureStatus ? extractHttpStatus(result.stdout) : null;
252
+ // "empty response body" only makes sense for the -i (captureStatus) path;
253
+ // for JSON/GET calls an empty stderr means fall back to the exit code.
254
+ const detail = result.stderr.trim()
255
+ || (captureStatus ? "empty response body" : `exit code ${result.code}`);
256
+ const statusPart = status ? ` (HTTP ${status})` : "";
257
+ throw new Error(`gh api command failed${statusPart} for ${endpoint}: ${detail}`);
236
258
  }
237
259
  if (!expectJson) {
238
260
  return null;
@@ -297,12 +319,26 @@ export async function runReorder({ repo, issue, order }, { env = process.env, gh
297
319
  }
298
320
  }
299
321
  const reorderPath = buildSubIssueReorderPath(owner, name, issue);
300
- let afterId = 0;
301
- for (const n of order) {
302
- const subIssueId = idByNumber.get(n);
322
+ // GitHub's priority endpoint rejects after_id=0 (the "no predecessor" cursor)
323
+ // with an HTTP 500. To place the first requested item at the head, move it
324
+ // before the current head instead — or skip the call entirely if it's
325
+ // already there.
326
+ const currentHeadId = subIssues[0]?.id;
327
+ let afterId;
328
+ for (let index = 0; index < order.length; index += 1) {
329
+ const subIssueId = idByNumber.get(order[index]);
330
+ if (index === 0) {
331
+ afterId = subIssueId;
332
+ if (subIssueId === currentHeadId) {
333
+ continue; // already at the head; no call needed
334
+ }
335
+ const fieldArgs = ["-F", `sub_issue_id=${subIssueId}`, "-F", `before_id=${currentHeadId}`];
336
+ await ghApi(ghCommand, ["-X", "PATCH", reorderPath, ...fieldArgs], env, false);
337
+ continue;
338
+ }
303
339
  const fieldArgs = ["-F", `sub_issue_id=${subIssueId}`, "-F", `after_id=${afterId}`];
304
- await ghApi(ghCommand, ["-X", "PATCH", reorderPath, ...fieldArgs], env, false);
305
340
  afterId = subIssueId;
341
+ await ghApi(ghCommand, ["-X", "PATCH", reorderPath, ...fieldArgs], env, false);
306
342
  }
307
343
  return {
308
344
  ok: true,
@@ -387,7 +423,7 @@ export async function runVerify(
387
423
  }
388
424
  export async function runCli(
389
425
  argv = process.argv.slice(2),
390
- { stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
426
+ { stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
391
427
  ) {
392
428
  const options = parseManageSubIssuesCliArgs(argv);
393
429
  if (options.help) {
@@ -405,7 +441,7 @@ export async function runCli(
405
441
  } else if (command === "verify") {
406
442
  result = await runVerify({ repo, issue, expected, ordered }, { env, ghCommand });
407
443
  }
408
- stdout.write(`${JSON.stringify(result)}\n`);
444
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
409
445
  }
410
446
  if (isDirectCliRun(import.meta.url)) {
411
447
  runCli().catch((error) => {
@@ -4,6 +4,7 @@ import { parsePrNumber, runChild } from "../_cli-primitives.mjs";
4
4
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
5
5
  import { loadDevLoopConfig } from "@dev-loops/core/config";
6
6
  import { resolveHandoffCandidates } from "./resolve-handoff-candidates.mjs";
7
+ import { JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
7
8
 
8
9
  const USAGE = `Usage: offer-human-handoff.mjs --repo <owner/name> --pr <number> [--assign <login>...] [--request-review <login>...] [--changed-files <csv>] [--pr-author <login>]
9
10
  Human-handoff offer at the pre-approval / merge-handoff boundary (#920, Request B
@@ -28,9 +29,11 @@ Optional:
28
29
  Output (stdout, JSON):
29
30
  Offer mode: { "ok": true, "mode": "offer", "enabled", "candidates": [...], ... }
30
31
  Apply mode: { "ok": true, "mode": "apply", "assigned": [...], "requestedReview": [...] }
32
+ ${JQ_OUTPUT_USAGE}
31
33
  Exit codes:
32
34
  0 Success (including disabled no-op offer)
33
- 1 Argument error or gh failure`.trim();
35
+ 1 Argument error or gh failure
36
+ 2 Invalid --jq filter`.trim();
34
37
 
35
38
  const parseError = buildParseError(USAGE);
36
39
 
@@ -70,6 +73,8 @@ export function parseOfferCliArgs(argv) {
70
73
  continue;
71
74
  }
72
75
  if (token === "--pr-author") { options.prAuthor = nextValue(args, ++i, "--pr-author").trim().replace(/^@/, ""); continue; }
76
+ if (token === "--jq") { options.jq = nextValue(args, ++i, "--jq"); continue; }
77
+ if (token === "--silent" || token === "-s") { options.silent = true; continue; }
73
78
  throw parseError(`Unknown argument: ${token}`);
74
79
  }
75
80
  if (options.repo === undefined || options.pr === undefined) {
@@ -123,8 +128,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
123
128
  { repo: options.repo, pr: options.pr, assign: options.assign, requestReview: options.requestReview },
124
129
  { run, ghCommand },
125
130
  );
126
- process.stdout.write(`${JSON.stringify(applied)}\n`);
127
- return 0;
131
+ return emitResult(applied, { jq: options.jq, silent: options.silent });
128
132
  } catch (error) {
129
133
  process.stderr.write(`${formatCliError(error)}\n`);
130
134
  return 1;
@@ -138,8 +142,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
138
142
  { repo: options.repo, pr: options.pr, changedFiles: options.changedFiles, prAuthor: options.prAuthor ?? null },
139
143
  { ...deps, config, repoRoot, run, ghCommand },
140
144
  );
141
- process.stdout.write(`${JSON.stringify({ ...offer, mode: "offer" })}\n`);
142
- return 0;
145
+ return emitResult({ ...offer, mode: "offer" }, { jq: options.jq, silent: options.silent });
143
146
  }
144
147
 
145
148
  if (isDirectCliRun(import.meta.url)) {
@@ -3,6 +3,7 @@ import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.
3
3
  import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
4
4
  import { loadDevLoopConfig, resolveGatePostFindingsComments } from "@dev-loops/core/config";
5
5
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
6
+ import { JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
6
7
 
7
8
  const USAGE = `Usage: post-gate-findings.mjs --repo <owner/name> --pr <number> --gate <draft_gate|pre_approval_gate> --head-sha <sha> --findings <json>
8
9
  Post (or idempotently update) a visible, marker-tagged PR issue comment that lists the
@@ -23,9 +24,12 @@ Required:
23
24
  ([{severity, angle, summary, disposition?, files?}])
24
25
  Output (stdout, JSON):
25
26
  { "ok": true, "action": "created"|"updated"|"noop"|"skipped", ... }
27
+
28
+ ${JQ_OUTPUT_USAGE}
26
29
  Exit codes:
27
30
  0 Success
28
- 1 Argument error or gh failure`.trim();
31
+ 1 Argument error or gh failure
32
+ 2 Invalid --jq filter`.trim();
29
33
 
30
34
  const VALID_SEVERITIES = new Set(["must-fix", "worth-fixing-now", "defer"]);
31
35
  // Severity ordering for grouped rendering (most-blocking first).
@@ -142,6 +146,14 @@ export function parsePostGateFindingsCliArgs(argv) {
142
146
  options.findings = requireOptionValue(args, "--findings", parseError);
143
147
  continue;
144
148
  }
149
+ if (token === "--jq") {
150
+ options.jq = requireOptionValue(args, "--jq", parseError);
151
+ continue;
152
+ }
153
+ if (token === "--silent" || token === "-s") {
154
+ options.silent = true;
155
+ continue;
156
+ }
145
157
  throw parseError(`Unknown argument: ${token}`);
146
158
  }
147
159
  const missing = ["repo", "pr", "gate", "headSha", "findings"]
@@ -380,7 +392,7 @@ async function main() {
380
392
  }
381
393
  try {
382
394
  const result = await postGateFindings(options);
383
- process.stdout.write(`${JSON.stringify(result)}\n`);
395
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
384
396
  } catch (error) {
385
397
  process.stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
386
398
  process.exitCode = 1;
@@ -4,6 +4,7 @@ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helper
4
4
  import { parseArgs } from "node:util";
5
5
  import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
6
6
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
7
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
7
8
  import {
8
9
  summarizeHeadScopedCheckRunsSignal,
9
10
  normalizeHeadScopedCommitStatus,
@@ -53,9 +54,11 @@ No-checks rule (grace, race-safe):
53
54
  Diagnostic output (stderr):
54
55
  { "ok": true, "type": "watch_heartbeat", "elapsedMs": N, "totalBudgetMs": N, "poll": N, "maxPolls": N }
55
56
  { "ok": false, "error": "...", "usage"?: "..." }
57
+ ${JQ_OUTPUT_USAGE}
56
58
  Exit codes:
57
59
  0 Success
58
- 1 Argument error or gh failure`.trim();
60
+ 1 Argument error or gh failure
61
+ 2 Invalid --jq filter`.trim();
59
62
  const parseError = buildParseError(USAGE);
60
63
 
61
64
  export function parseCiWatchCliArgs(argv) {
@@ -67,6 +70,7 @@ export function parseCiWatchCliArgs(argv) {
67
70
  pr: { type: "string" },
68
71
  "timeout-ms": { type: "string" },
69
72
  "poll-interval-ms": { type: "string" },
73
+ ...JQ_OUTPUT_PARSE_OPTIONS,
70
74
  },
71
75
  allowPositionals: true,
72
76
  strict: false,
@@ -106,6 +110,7 @@ export function parseCiWatchCliArgs(argv) {
106
110
  options.pollIntervalMs = parsePositiveMs(requireTokenValue(token, parseError), "--poll-interval-ms");
107
111
  continue;
108
112
  }
113
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
109
114
  throw parseError(`Unknown argument: ${token.rawName}`);
110
115
  }
111
116
  if (options.repo === undefined || options.pr === undefined) {
@@ -447,6 +452,7 @@ export async function runCli(
447
452
  argv = process.argv.slice(2),
448
453
  {
449
454
  stdout = process.stdout,
455
+ stderr = process.stderr,
450
456
  env = process.env,
451
457
  ghCommand = "gh",
452
458
  } = {},
@@ -457,7 +463,7 @@ export async function runCli(
457
463
  return;
458
464
  }
459
465
  const result = await watchCiStatus(options, { env, ghCommand });
460
- stdout.write(`${JSON.stringify(result)}\n`);
466
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
461
467
  }
462
468
 
463
469
  if (isDirectCliRun(import.meta.url)) {
@@ -2,8 +2,8 @@
2
2
  import { setTimeout as delay } from "node:timers/promises";
3
3
  import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, parseJsonText, parseReviewThreads } from "../_core-helpers.mjs";
4
4
  import { parseArgs } from "node:util";
5
- import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
6
- import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
5
+ import { parsePositiveInteger, parseNonNegativeInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
6
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
7
7
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
8
8
  import {
9
9
  DEFAULT_POLL_INTERVAL_MS,
@@ -15,13 +15,18 @@ import {
15
15
  const WATCH_HEARTBEAT_MS = 45_000; // 45 seconds
16
16
  const REMOVED_FLAGS = new Set([
17
17
  "--poll-interval-ms",
18
- "--timeout-ms",
19
18
  ]);
20
- const USAGE = `Usage: probe-copilot-review.mjs --repo <owner/name> --pr <number>
19
+ const USAGE = `Usage: probe-copilot-review.mjs --repo <owner/name> --pr <number> [--timeout-ms <n>]
21
20
  Poll for fresh Copilot review activity on a GitHub pull request.
22
21
  Required:
23
22
  --repo <owner/name> Repository slug (e.g. owner/repo)
24
23
  --pr <number> Pull request number
24
+ Options:
25
+ --timeout-ms <n> Total watch budget in ms (default ${COPILOT_REVIEW_WAIT_TIMEOUT_MS}, i.e. ${COPILOT_REVIEW_WAIT_TIMEOUT_MS / 60_000} min;
26
+ 0 = single immediate check, no wait — returns "idle" if
27
+ no fresh activity). For a quick non-watch thread/state
28
+ read without entering the watch loop, pass 0 here or use
29
+ 'dev-loops gate capture-threads'.
25
30
  Output (stdout, JSON):
26
31
  { "ok": true, "status": "changed"|"timeout"|"idle", "repo": "...", "pr": N, "attempts": N,
27
32
  "newComments": [...], "newReviews": [...], "newIssueComments": [...] }
@@ -32,10 +37,14 @@ ${JQ_OUTPUT_USAGE}
32
37
  Activity statuses:
33
38
  changed Fresh Copilot review activity found (check newComments/newReviews/newIssueComments)
34
39
  timeout Watch period elapsed with no fresh Copilot activity
35
- idle Zero-timeout single check found no change
40
+ idle Zero-timeout (--timeout-ms 0) single check found no change
36
41
  Diagnostic output (stderr):
37
- Progress/heartbeat (during watch):
42
+ Progress/heartbeat (during watch, --timeout-ms > 0):
38
43
  { "ok": true, "type": "watch_heartbeat", "elapsedMs": N, "totalBudgetMs": N, "poll": N, "maxPolls": N }
44
+ Heartbeat contract: watch-shaped runs (--timeout-ms > 0) emit watch_heartbeat lines to
45
+ stderr roughly every 45s as a liveness signal. Agents MUST NOT suppress stderr
46
+ (e.g. 2>/dev/null) on watch-shaped invocations — suppressing heartbeats makes a
47
+ legitimate watch look like a stall. Use --timeout-ms 0 for a non-watch single check.
39
48
  Argument/usage errors:
40
49
  { "ok": false, "error": "...", "usage": "..." }
41
50
  gh/runtime failures:
@@ -100,6 +109,7 @@ export function parseWatchCliArgs(argv) {
100
109
  help: { type: "boolean", short: "h" },
101
110
  repo: { type: "string" },
102
111
  pr: { type: "string" },
112
+ "timeout-ms": { type: "string" },
103
113
  concise: { type: "boolean" },
104
114
  summary: { type: "boolean" },
105
115
  ...JQ_OUTPUT_PARSE_OPTIONS,
@@ -140,18 +150,15 @@ export function parseWatchCliArgs(argv) {
140
150
  options.pr = parsePositiveInteger(requireTokenValue(token, parseError), "--pr", parseError);
141
151
  continue;
142
152
  }
143
- if (token.name === "concise" || token.name === "summary") {
144
- options.concise = true;
145
- continue;
146
- }
147
- if (token.name === "jq") {
148
- options.jq = requireTokenValue(token, parseError);
153
+ if (token.name === "timeout-ms") {
154
+ options.timeoutMs = parseNonNegativeInteger(requireTokenValue(token, parseError), "--timeout-ms", parseError);
149
155
  continue;
150
156
  }
151
- if (token.name === "silent") {
152
- options.silent = true;
157
+ if (token.name === "concise" || token.name === "summary") {
158
+ options.concise = true;
153
159
  continue;
154
160
  }
161
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
155
162
  throw parseError(`Unknown argument: ${token.rawName}`);
156
163
  }
157
164
  if (options.repo === undefined || options.pr === undefined) {
@@ -5,15 +5,17 @@ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.m
5
5
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
6
6
  import { loadDevLoopConfig, resolveGateConfig } from "@dev-loops/core/config";
7
7
  import { findBlockingTitleMarkers } from "@dev-loops/core/loop/pr-title-markers";
8
+ import { syncBoardStatus as realSyncBoardStatus, loadStateColumnMap, LOGICAL_COLUMN } from "@dev-loops/core/loop/queue-board-sync";
9
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
8
10
 
9
- const USAGE = `Usage: ready-for-review.mjs --repo <owner/name> --pr <number>\nWrapper around gh pr ready that enforces gate-evidence validation.`;
11
+ const USAGE = `Usage: ready-for-review.mjs --repo <owner/name> --pr <number>\nWrapper around gh pr ready that enforces gate-evidence validation.\n\n${JQ_OUTPUT_USAGE}`;
10
12
  const parseError = buildParseError(USAGE);
11
- const PR_VIEW_QUERY = `query($owner:String!, $name:String!, $number:Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$number) { id, isDraft, headRefOid, state, mergeStateStatus, title } } }`;
13
+ const PR_VIEW_QUERY = `query($owner:String!, $name:String!, $number:Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$number) { id, isDraft, headRefOid, state, mergeStateStatus, title, closingIssuesReferences(first:10){ nodes{ number } } } } }`;
12
14
 
13
15
  export function parseReadyForReviewCliArgs(argv) {
14
16
  const { tokens } = parseArgs({
15
17
  args: [...argv],
16
- options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" } },
18
+ options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" }, ...JQ_OUTPUT_PARSE_OPTIONS },
17
19
  allowPositionals: true,
18
20
  strict: false,
19
21
  tokens: true,
@@ -25,6 +27,7 @@ export function parseReadyForReviewCliArgs(argv) {
25
27
  if (token.name === "help") { opts.help = true; return opts; }
26
28
  if (token.name === "repo") { opts.repo = requireTokenValue(token, parseError).trim(); continue; }
27
29
  if (token.name === "pr") { opts.pr = parsePrNumber(requireTokenValue(token, parseError), parseError); continue; }
30
+ if (matchJqOutputToken(token, opts, (t) => requireTokenValue(t, parseError))) continue;
28
31
  throw parseError(`Unknown argument: ${token.rawName}`);
29
32
  }
30
33
  if (!opts.repo || opts.pr === undefined) throw parseError("ready-for-review requires --repo and --pr");
@@ -43,7 +46,10 @@ async function fetchPrState({ repo, pr }, { env, ghCommand }) {
43
46
  const r = await runGhJson(["api", "graphql", "-f", `query=${PR_VIEW_QUERY}`, "-f", `owner=${owner}`, "-f", `name=${name}`, "-F", `number=${pr}`], { env, ghCommand });
44
47
  const d = r?.data?.repository?.pullRequest;
45
48
  if (!d) throw new Error(`Could not fetch PR #${pr}`);
46
- return { id: d.id, isDraft: d.isDraft === true, headRefOid: typeof d.headRefOid === "string" ? d.headRefOid.trim() : null, state: typeof d.state === "string" ? d.state.trim() : null, mergeStateStatus: typeof d.mergeStateStatus === "string" ? d.mergeStateStatus.trim() : null, title: typeof d.title === "string" ? d.title : null };
49
+ const closingIssues = (d.closingIssuesReferences?.nodes ?? [])
50
+ .map((n) => n?.number)
51
+ .filter((n) => Number.isInteger(n) && n > 0);
52
+ return { id: d.id, isDraft: d.isDraft === true, headRefOid: typeof d.headRefOid === "string" ? d.headRefOid.trim() : null, state: typeof d.state === "string" ? d.state.trim() : null, mergeStateStatus: typeof d.mergeStateStatus === "string" ? d.mergeStateStatus.trim() : null, title: typeof d.title === "string" ? d.title : null, closingIssues };
47
53
  }
48
54
 
49
55
  async function fetchCiStatus({ repo, pr }, { env, ghCommand }) {
@@ -73,7 +79,7 @@ async function fetchGateEvidence({ repo, pr, headSha }, { env, ghCommand }) {
73
79
  return { draftGate: dg, draftGateMarker: dm, currentHeadClean: chc, cleanEvidenceExists: cee, effectiveHeadClean: chc || cphm };
74
80
  }
75
81
 
76
- export async function readyForReview(options, { env = process.env, ghCommand = "gh", repoRoot = process.cwd() } = {}) {
82
+ export async function readyForReview(options, { env = process.env, ghCommand = "gh", repoRoot = process.cwd(), syncBoardStatus = realSyncBoardStatus } = {}) {
77
83
  const { config } = await loadDevLoopConfig({ repoRoot });
78
84
  const draftGateConfig = resolveGateConfig(config, "draft");
79
85
  const requireCi = draftGateConfig?.requireCi !== false;
@@ -89,15 +95,27 @@ export async function readyForReview(options, { env = process.env, ghCommand = "
89
95
  if (!gate.effectiveHeadClean) { const mv = gate.draftGateMarker?.visible; const mh = gate.draftGateMarker?.headSha; throw new Error(mv && mh ? `PR #${options.pr} draft_gate marker does not match current head ${headSha.slice(0,7)}. Re-run draft gate.` : `PR #${options.pr} draft_gate marker is missing or incomplete on current head ${headSha.slice(0,7)}. Re-run draft gate.`); }
90
96
  const readyResult = await runChild(ghCommand, ["pr", "ready", String(options.pr), "--repo", options.repo], env);
91
97
  if (readyResult.code !== 0) throw new Error(`gh pr ready failed`);
92
- return { ok: true, action: "marked_ready", repo: options.repo, pr: options.pr, headSha, draftGateSatisfied: gate.effectiveHeadClean };
98
+ // #1069: couple the In-Progress board move to the ready transition. Best-effort
99
+ // and NON-FATAL — a board failure must NEVER block or fail marking ready.
100
+ let boardSync;
101
+ try {
102
+ const inProgressColumn = loadStateColumnMap(repoRoot).columnNames[LOGICAL_COLUMN.IN_PROGRESS];
103
+ const targets = prState.closingIssues.length > 0 ? prState.closingIssues : [options.pr];
104
+ boardSync = [];
105
+ for (const target of targets) {
106
+ boardSync.push(await syncBoardStatus(options.repo, repoRoot, target, inProgressColumn, env, {}));
107
+ }
108
+ } catch (err) {
109
+ boardSync = [{ ok: true, skipped: true, reason: err?.message ?? "board sync failed" }];
110
+ }
111
+ return { ok: true, action: "marked_ready", repo: options.repo, pr: options.pr, headSha, draftGateSatisfied: gate.effectiveHeadClean, boardSync };
93
112
  }
94
113
 
95
114
  export async function main(argv = process.argv.slice(2), runtime = {}) {
96
115
  const options = parseReadyForReviewCliArgs(argv);
97
116
  if (options.help) { process.stdout.write(`${USAGE}\n`); return 0; }
98
117
  const result = await readyForReview(options, runtime);
99
- process.stdout.write(`${JSON.stringify(result)}\n`);
100
- return 0;
118
+ return emitResult(result, { jq: options.jq, silent: options.silent });
101
119
  }
102
120
 
103
121
  if (isDirectCliRun(import.meta.url)) {
@@ -6,6 +6,7 @@ import { loadDevLoopConfig, resolveGateConfig } from "@dev-loops/core/config";
6
6
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
7
7
  import { detectCheckpointEvidence } from "./detect-checkpoint-evidence.mjs";
8
8
  import { upsertCheckpointVerdict } from "./upsert-checkpoint-verdict.mjs";
9
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
9
10
  const USAGE = `Usage: reconcile-draft-gate.mjs --repo <owner/name> --pr <number>
10
11
  Optional/manual recovery tool for an already non-draft PR when you want to
11
12
  retroactively record clean \`draft_gate\` evidence.
@@ -32,14 +33,16 @@ Output (stdout, JSON):
32
33
  Error output (stderr, JSON):
33
34
  { "ok": false, "error": "...", "usage": "..." }
34
35
  { "ok": false, "error": "..." }
36
+ ${JQ_OUTPUT_USAGE}
35
37
  Exit codes:
36
38
  0 Success — PR was reconciled and gate evidence posted
37
- 1 Argument error, gh failure, or unrecoverable state`.trim();
39
+ 1 Argument error, gh failure, or unrecoverable state
40
+ 2 Invalid --jq filter`.trim();
38
41
  const parseError = buildParseError(USAGE);
39
42
  export function parseReconcileDraftGateCliArgs(argv) {
40
43
  const { tokens } = parseArgs({
41
44
  args: [...argv],
42
- options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" } },
45
+ options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" }, ...JQ_OUTPUT_PARSE_OPTIONS },
43
46
  allowPositionals: true,
44
47
  strict: false,
45
48
  tokens: true,
@@ -68,6 +71,7 @@ export function parseReconcileDraftGateCliArgs(argv) {
68
71
  options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
69
72
  continue;
70
73
  }
74
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
71
75
  throw parseError(`Unknown argument: ${token.rawName}`);
72
76
  }
73
77
  const missing = ["repo", "pr"].filter((key) => options[key] === undefined);
@@ -327,7 +331,7 @@ async function main() {
327
331
  }
328
332
  try {
329
333
  const result = await reconcileDraftGate(options);
330
- process.stdout.write(`${JSON.stringify(result)}\n`);
334
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
331
335
  } catch (error) {
332
336
  process.stderr.write(
333
337
  `${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`