dev-loops 0.6.0 → 0.7.2

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 (203) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +8 -7
  3. package/.claude/agents/fixer.md +1 -0
  4. package/.claude/agents/refiner.md +2 -2
  5. package/.claude/agents/review.md +4 -3
  6. package/.claude/commands/{auto.md → loop-auto.md} +1 -1
  7. package/.claude/commands/loop-continue.md +15 -0
  8. package/.claude/commands/loop-enqueue.md +22 -0
  9. package/.claude/commands/loop-grill.md +17 -0
  10. package/.claude/commands/{info.md → loop-info.md} +2 -2
  11. package/.claude/commands/loop-queue-status.md +24 -0
  12. package/.claude/commands/{start-spike.md → loop-start-spike.md} +2 -2
  13. package/.claude/commands/{start.md → loop-start.md} +1 -1
  14. package/.claude/commands/{status.md → loop-status.md} +1 -1
  15. package/.claude/hooks/_bash-command-classify.mjs +333 -29
  16. package/.claude/hooks/_hook-decisions.mjs +138 -15
  17. package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
  18. package/.claude/skills/copilot-pr-followup/SKILL.md +60 -34
  19. package/.claude/skills/dev-loop/SKILL.md +16 -12
  20. package/.claude/skills/docs/acceptance-criteria-verification.md +8 -3
  21. package/.claude/skills/docs/anti-patterns.md +5 -3
  22. package/.claude/skills/docs/artifact-authority-contract.md +30 -11
  23. package/.claude/skills/docs/confirmation-rules.md +1 -0
  24. package/.claude/skills/docs/contract-style-guide.md +37 -0
  25. package/.claude/skills/docs/copilot-loop-operations.md +19 -15
  26. package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
  27. package/.claude/skills/docs/debt-remediation-contract.md +1 -1
  28. package/.claude/skills/docs/epic-tree-refinement-procedure.md +25 -22
  29. package/.claude/skills/docs/issue-intake-procedure.md +26 -19
  30. package/.claude/skills/docs/local-planning-flow.md +1 -1
  31. package/.claude/skills/docs/local-planning-worked-example.md +1 -1
  32. package/.claude/skills/docs/merge-preconditions.md +30 -12
  33. package/.claude/skills/docs/plan-file-contract.md +1 -1
  34. package/.claude/skills/docs/pr-lifecycle-contract.md +44 -35
  35. package/.claude/skills/docs/public-dev-loop-contract.md +60 -44
  36. package/.claude/skills/docs/retrospective-checkpoint-contract.md +106 -79
  37. package/.claude/skills/docs/spike-mode-contract.md +14 -7
  38. package/.claude/skills/docs/stop-conditions.md +30 -15
  39. package/.claude/skills/docs/tracker-first-loop-state.md +13 -8
  40. package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
  41. package/.claude/skills/docs/validation-policy.md +4 -5
  42. package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
  43. package/.claude/skills/local-implementation/SKILL.md +62 -145
  44. package/.claude/skills/loop-grill/SKILL.md +163 -0
  45. package/AGENTS.md +1 -0
  46. package/CHANGELOG.md +101 -0
  47. package/README.md +9 -9
  48. package/agents/dev-loop.agent.md +11 -10
  49. package/agents/developer.agent.md +1 -1
  50. package/agents/docs.agent.md +1 -1
  51. package/agents/fixer.agent.md +2 -1
  52. package/agents/quality.agent.md +1 -1
  53. package/agents/refiner.agent.md +3 -3
  54. package/agents/review.agent.md +5 -4
  55. package/cli/index.mjs +35 -42
  56. package/extension/README.md +4 -4
  57. package/extension/checks.ts +1 -5
  58. package/extension/harness-types.ts +1 -0
  59. package/extension/index.ts +6 -0
  60. package/extension/pi-extension-adapter.ts +2 -0
  61. package/extension/presentation.ts +7 -19
  62. package/package.json +12 -12
  63. package/scripts/_core-helpers.mjs +2 -0
  64. package/scripts/claude/headless-dev-loop.mjs +1 -1
  65. package/scripts/docs/validate-rule-ownership.mjs +442 -0
  66. package/scripts/docs/validate-state-machine-conformance.mjs +1092 -0
  67. package/scripts/github/_review-thread-mutations.mjs +5 -2
  68. package/scripts/github/capture-review-threads.mjs +2 -9
  69. package/scripts/github/comment-issue.mjs +2 -9
  70. package/scripts/github/create-label.mjs +133 -0
  71. package/scripts/github/create-pr.mjs +119 -10
  72. package/scripts/github/detect-checkpoint-evidence.mjs +259 -18
  73. package/scripts/github/detect-linked-issue-pr.mjs +22 -6
  74. package/scripts/github/edit-pr.mjs +259 -0
  75. package/scripts/github/fetch-ci-logs.mjs +2 -9
  76. package/scripts/github/list-issues.mjs +2 -9
  77. package/scripts/github/list-review-threads.mjs +277 -0
  78. package/scripts/github/manage-sub-issues.mjs +46 -10
  79. package/scripts/github/offer-human-handoff.mjs +8 -5
  80. package/scripts/github/post-gate-findings.mjs +20 -5
  81. package/scripts/github/probe-ci-status.mjs +8 -2
  82. package/scripts/github/probe-copilot-review.mjs +21 -14
  83. package/scripts/github/ready-for-review.mjs +26 -8
  84. package/scripts/github/reconcile-draft-gate.mjs +7 -3
  85. package/scripts/github/reply-resolve-review-thread.mjs +16 -5
  86. package/scripts/github/reply-resolve-review-threads.mjs +69 -7
  87. package/scripts/github/request-copilot-review.mjs +98 -26
  88. package/scripts/github/resolve-handoff-candidates.mjs +7 -3
  89. package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
  90. package/scripts/github/stage-reviewer-draft.mjs +10 -2
  91. package/scripts/github/tick-verified-checkboxes.mjs +202 -0
  92. package/scripts/github/upsert-checkpoint-verdict.mjs +105 -15
  93. package/scripts/github/verify-briefing-prefixes.mjs +191 -0
  94. package/scripts/github/verify-fresh-review-context.mjs +226 -32
  95. package/scripts/github/view-pr.mjs +150 -0
  96. package/scripts/github/wait-pr-checks.mjs +171 -0
  97. package/scripts/github/write-gate-context.mjs +458 -65
  98. package/scripts/github/write-gate-findings-log.mjs +128 -2
  99. package/scripts/lib/jq-output.mjs +18 -0
  100. package/scripts/loop/_post-convergence-change.mjs +211 -0
  101. package/scripts/loop/_pr-runner-coordination.mjs +70 -0
  102. package/scripts/loop/_repo-root-resolver.mjs +47 -0
  103. package/scripts/loop/build-handoff-envelope.mjs +14 -4
  104. package/scripts/loop/check-retro-tooling.mjs +14 -3
  105. package/scripts/loop/checkpoint-contract.mjs +7 -4
  106. package/scripts/loop/cleanup-worktree.mjs +12 -3
  107. package/scripts/loop/conductor-monitor.mjs +12 -18
  108. package/scripts/loop/copilot-pr-handoff.mjs +162 -14
  109. package/scripts/loop/debt-remediate.mjs +24 -12
  110. package/scripts/loop/detect-change-scope.mjs +38 -9
  111. package/scripts/loop/detect-copilot-loop-state.mjs +34 -8
  112. package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
  113. package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
  114. package/scripts/loop/detect-internal-only-pr.mjs +8 -2
  115. package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
  116. package/scripts/loop/detect-pr-gate-coordination-state.mjs +184 -69
  117. package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
  118. package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
  119. package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
  120. package/scripts/loop/ensure-worktree.mjs +8 -3
  121. package/scripts/loop/info.mjs +12 -4
  122. package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
  123. package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
  124. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
  125. package/scripts/loop/inspect-run-viewer.mjs +67 -4
  126. package/scripts/loop/inspect-run.mjs +8 -2
  127. package/scripts/loop/outer-loop.mjs +8 -4
  128. package/scripts/loop/pr-runner-coordination.mjs +2 -9
  129. package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
  130. package/scripts/loop/pre-flight-gate.mjs +9 -9
  131. package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
  132. package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
  133. package/scripts/loop/provision-worktree.mjs +74 -3
  134. package/scripts/loop/resolve-dev-loop-startup.mjs +216 -10
  135. package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
  136. package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
  137. package/scripts/loop/run-conductor-cycle.mjs +8 -2
  138. package/scripts/loop/run-queue.mjs +18 -9
  139. package/scripts/loop/run-refinement-audit.mjs +8 -9
  140. package/scripts/loop/run-watch-cycle.mjs +2 -9
  141. package/scripts/loop/sanctioned-commands.mjs +106 -0
  142. package/scripts/loop/steer-loop.mjs +29 -16
  143. package/scripts/loop/validate-pr-body-spec.mjs +223 -0
  144. package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
  145. package/scripts/pages/build-site.mjs +59 -8
  146. package/scripts/pages/build-state-atlas.mjs +443 -0
  147. package/scripts/projects/_resolve-project.mjs +1 -0
  148. package/scripts/projects/add-queue-item.mjs +60 -54
  149. package/scripts/projects/archive-done-items.mjs +49 -84
  150. package/scripts/projects/ensure-queue-board.mjs +10 -36
  151. package/scripts/projects/list-queue-items.mjs +59 -382
  152. package/scripts/projects/move-queue-item.mjs +21 -449
  153. package/scripts/projects/reconcile-queue.mjs +253 -0
  154. package/scripts/projects/reorder-queue-item.mjs +43 -68
  155. package/scripts/projects/resolve-active-board-item.mjs +108 -46
  156. package/scripts/projects/sync-item-status.mjs +15 -10
  157. package/scripts/refine/_refine-helpers.mjs +21 -5
  158. package/scripts/refine/exit-spike.mjs +18 -8
  159. package/scripts/refine/promote-plan.mjs +22 -16
  160. package/scripts/refine/prose-linkage-detector.mjs +1 -1
  161. package/scripts/refine/refine-plan-file.mjs +13 -4
  162. package/scripts/refine/refinement-completeness-checker.mjs +1 -1
  163. package/scripts/refine/scaffold-spike-file.mjs +4 -8
  164. package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
  165. package/scripts/refine/tree-integrity-validator.mjs +1 -1
  166. package/scripts/refine/validate-plan-file.mjs +1 -1
  167. package/scripts/refine/validate-spike-file.mjs +1 -1
  168. package/scripts/refine/verify.mjs +11 -6
  169. package/scripts/release/assert-core-dependency-version.mjs +123 -0
  170. package/scripts/release/extract-changelog-section.mjs +16 -2
  171. package/skills/copilot-pr-followup/SKILL.md +60 -34
  172. package/skills/dev-loop/SKILL.md +11 -7
  173. package/skills/docs/acceptance-criteria-verification.md +8 -3
  174. package/skills/docs/anti-patterns.md +5 -3
  175. package/skills/docs/artifact-authority-contract.md +30 -11
  176. package/skills/docs/confirmation-rules.md +1 -0
  177. package/skills/docs/contract-style-guide.md +37 -0
  178. package/skills/docs/copilot-loop-operations.md +19 -15
  179. package/skills/docs/cross-harness-regression-contract.md +60 -0
  180. package/skills/docs/debt-remediation-contract.md +1 -1
  181. package/skills/docs/epic-tree-refinement-procedure.md +25 -22
  182. package/skills/docs/issue-intake-procedure.md +26 -19
  183. package/skills/docs/local-planning-flow.md +1 -1
  184. package/skills/docs/local-planning-worked-example.md +1 -1
  185. package/skills/docs/merge-preconditions.md +30 -12
  186. package/skills/docs/plan-file-contract.md +1 -1
  187. package/skills/docs/pr-lifecycle-contract.md +44 -35
  188. package/skills/docs/public-dev-loop-contract.md +60 -44
  189. package/skills/docs/required-rules.json +149 -0
  190. package/skills/docs/retrospective-checkpoint-contract.md +106 -79
  191. package/skills/docs/spike-mode-contract.md +14 -7
  192. package/skills/docs/stop-conditions.md +30 -15
  193. package/skills/docs/tracker-first-loop-state.md +13 -8
  194. package/skills/docs/ui-e2e-scoping-step.md +32 -0
  195. package/skills/docs/validation-policy.md +4 -5
  196. package/skills/docs/workflow-handoff-contract.md +39 -0
  197. package/skills/local-implementation/SKILL.md +62 -145
  198. package/skills/loop-grill/SKILL.md +165 -0
  199. package/.claude/commands/continue.md +0 -15
  200. package/scripts/docs/validate-no-duplicate-rules.mjs +0 -250
  201. package/scripts/loop/conductor.mjs +0 -233
  202. package/scripts/loop/detect-stale-runner.mjs +0 -265
  203. package/scripts/loop/pre-push-main-guard.mjs +0 -117
@@ -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)) {
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
3
- import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
3
+ import { formatCliError, isDirectCliRun, parseJsonText, sanitizeCopilotSummonTokens } 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"]
@@ -203,7 +215,7 @@ export function renderFindingsCommentBody({ gate, headSha, findings }) {
203
215
  ];
204
216
  if (findings.length === 0) {
205
217
  lines.push("No findings. All review angles passed for this head.");
206
- return lines.join("\n");
218
+ return sanitizeCopilotSummonTokens(lines.join("\n"));
207
219
  }
208
220
  const grouped = new Map();
209
221
  for (const sev of SEVERITY_ORDER) {
@@ -249,7 +261,10 @@ export function renderFindingsCommentBody({ gate, headSha, findings }) {
249
261
  while (lines.length > 0 && lines[lines.length - 1] === "") {
250
262
  lines.pop();
251
263
  }
252
- return lines.join("\n");
264
+ // Neutralize any bare @copilot/`/copilot`* tokens a finding summary quotes
265
+ // (e.g. an excerpt of the anti-summon rule itself) so this comment can never
266
+ // arm request-copilot-review.mjs's anti-summon guard.
267
+ return sanitizeCopilotSummonTokens(lines.join("\n"));
253
268
  }
254
269
 
255
270
  async function runGhJson(args, { env, ghCommand }) {
@@ -380,7 +395,7 @@ async function main() {
380
395
  }
381
396
  try {
382
397
  const result = await postGateFindings(options);
383
- process.stdout.write(`${JSON.stringify(result)}\n`);
398
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
384
399
  } catch (error) {
385
400
  process.stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
386
401
  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`
@@ -9,6 +9,7 @@ import {
9
9
  replyAndMaybeResolve,
10
10
  validateResolutionMessage,
11
11
  } from "./_review-thread-mutations.mjs";
12
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
12
13
 
13
14
  export { hasCommitShaReference } from "./_review-thread-mutations.mjs";
14
15
 
@@ -21,7 +22,9 @@ Required:
21
22
  --pr <n> Pull request number
22
23
  --comment-id <n> GraphQL databaseId of the comment to reply to
23
24
  --thread-id <id> GraphQL node ID of the review thread
24
- --body-file <path> Path to file containing the reply body text`;
25
+ --body-file <path> Path to file containing the reply body text
26
+
27
+ ${JQ_OUTPUT_USAGE}`;
25
28
 
26
29
  function parseError(message) {
27
30
  return Object.assign(new Error(message), { usage: USAGE });
@@ -39,6 +42,7 @@ function parseCliArgs(argv) {
39
42
  "thread-id": { type: "string" },
40
43
  "body-file": { type: "string" },
41
44
  help: { type: "boolean", short: "h" },
45
+ ...JQ_OUTPUT_PARSE_OPTIONS,
42
46
  },
43
47
  strict: true,
44
48
  allowPositionals: false,
@@ -62,7 +66,15 @@ function parseCliArgs(argv) {
62
66
  const pr = parsePositiveInteger(values.pr, "--pr", parseError);
63
67
  const commentId = parsePositiveInteger(values["comment-id"], "--comment-id", parseError);
64
68
 
65
- return { repo: repoSlug, pr, commentId, threadId: values["thread-id"], bodyFile: values["body-file"] };
69
+ return {
70
+ repo: repoSlug,
71
+ pr,
72
+ commentId,
73
+ threadId: values["thread-id"],
74
+ bodyFile: values["body-file"],
75
+ jq: values.jq,
76
+ silent: values.silent === true,
77
+ };
66
78
  }
67
79
 
68
80
  async function run(argv) {
@@ -82,11 +94,10 @@ async function run(argv) {
82
94
  { env: process.env, ghCommand: "gh" },
83
95
  );
84
96
 
85
- process.stdout.write(JSON.stringify({
97
+ return emitResult({
86
98
  ok: true, repo: repoSlug, pr, commentId, threadId,
87
99
  replyId: result.replyId, replyUrl: result.replyUrl, resolved: true,
88
- }) + "\n");
89
- return 0;
100
+ }, { jq: parsed.jq, silent: parsed.silent });
90
101
  }
91
102
 
92
103
  if (isDirectCliRun(import.meta.url)) {