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
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { readFile } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import {
5
4
  buildParseError,
@@ -12,15 +11,18 @@ import {
12
11
  summarizeCopilotReviews,
13
12
  } from "../_core-helpers.mjs";
14
13
  import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
15
- import { loadDevLoopConfig, resolveGateConfig, resolveRefinement, resolveRefinementConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
14
+ import { loadDevLoopConfig, resolveEffectiveCopilotRoundCap, resolveGateConfig, resolveRefinement, resolveRefinementConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
16
15
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
17
- import { buildSnapshotFromPrFacts, interpretLoopState, summarizeLoopInterpretation } from "@dev-loops/core/loop/copilot-loop-state";
16
+ import { buildSnapshotFromPrFacts, interpretLoopState, isCopilotRoundCapReached, summarizeLoopInterpretation } from "@dev-loops/core/loop/copilot-loop-state";
18
17
  import { evaluatePrGateCoordination, PR_CHECKPOINT, PR_CHECKPOINT_ACTION } from "@dev-loops/core/loop/pr-gate-coordination";
19
18
  import { shouldGuardCopilotReviewRequest } from "@dev-loops/core/loop/pr-gate-coordination";
20
19
  import { UI_E2E_CHECK_NAMES } from "@dev-loops/core/loop/ui-e2e-scoping";
21
20
  import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
21
+ import { detectPostConvergenceSignificantChange } from "./_post-convergence-change.mjs";
22
22
  import { detectCheckpointEvidence } from "../github/detect-checkpoint-evidence.mjs";
23
+ import { resolveRepoRoot } from "./_repo-root-resolver.mjs";
23
24
  import { parseArgs } from "node:util";
25
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
24
26
  const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
25
27
  const USAGE = `Usage: detect-pr-gate-coordination-state.mjs --repo <owner/name> --pr <number>
26
28
  Determine which PR gate/transition is legal next for a pull request.
@@ -28,6 +30,12 @@ Required:
28
30
  --repo <owner/name> Repository slug (e.g. owner/repo)
29
31
  --pr <number> Pull request number
30
32
  Optional:
33
+ --lightweight This PR is light-dispatched (#1210): compose the Copilot
34
+ round cap with localImplementation.lightMode.maxCopilotRounds
35
+ (default 1) via min(lightMode.maxCopilotRounds,
36
+ refinement.maxCopilotRounds) instead of using
37
+ refinement.maxCopilotRounds alone. refinement.maxCopilotRounds:
38
+ 0 still disables Copilot rounds even with --lightweight.
31
39
  Output (stdout, JSON):
32
40
  {
33
41
  "ok": true,
@@ -67,15 +75,18 @@ Output (stdout, JSON):
67
75
  Error output (stderr, JSON):
68
76
  { "ok": false, "error": "...", "usage": "..." }
69
77
  { "ok": false, "error": "..." }
78
+ ${JQ_OUTPUT_USAGE}
70
79
  Exit codes:
71
80
  0 Success
72
- 1 Argument error or gh/runtime failure`.trim();
81
+ 1 Argument error or gh/runtime failure
82
+ 2 Invalid --jq filter`.trim();
73
83
  const parseError = buildParseError(USAGE);
74
84
  export function parseDetectPrGateCoordinationCliArgs(argv) {
75
85
  const options = {
76
86
  help: false,
77
87
  repo: undefined,
78
88
  pr: undefined,
89
+ lightweight: false,
79
90
  };
80
91
  const { tokens } = parseArgs({
81
92
  args: [...argv],
@@ -83,6 +94,8 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
83
94
  help: { type: "boolean", short: "h" },
84
95
  repo: { type: "string" },
85
96
  pr: { type: "string" },
97
+ lightweight: { type: "boolean" },
98
+ ...JQ_OUTPUT_PARSE_OPTIONS,
86
99
  },
87
100
  allowPositionals: true,
88
101
  strict: false,
@@ -107,6 +120,11 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
107
120
  options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
108
121
  continue;
109
122
  }
123
+ if (token.name === "lightweight") {
124
+ options.lightweight = true;
125
+ continue;
126
+ }
127
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
110
128
  throw parseError(`Unknown argument: ${token.rawName}`);
111
129
  }
112
130
  if (options.repo === undefined || options.pr === undefined) {
@@ -221,30 +239,38 @@ export function deriveUiE2ePassed(prData, checkNames = UI_E2E_CHECK_NAMES) {
221
239
  return present.every((entry) => {
222
240
  const conclusion = String(entry?.conclusion ?? "").toUpperCase();
223
241
  const state = String(entry?.state ?? "").toUpperCase();
224
- return conclusion === "SUCCESS" || state === "SUCCESS";
242
+ // SKIPPED = "not applicable to this run" (e.g. viewer-smoke when no viewer files changed) — not a failure.
243
+ return conclusion === "SUCCESS" || conclusion === "SKIPPED" || state === "SUCCESS" || state === "SKIPPED";
225
244
  });
226
245
  }
227
246
 
228
- export function resolveLinkedIssueFromPr(prData) {
229
- if (!prData || typeof prData !== "object") return null;
247
+ // Ordered, de-duplicated list of ALL closing-referenced issue numbers for a PR.
248
+ // Umbrella PRs legitimately close multiple issues (#1052), so the refinement
249
+ // guard resolves against every one of them, not just a unique single ref.
250
+ export function resolveLinkedIssuesFromPr(prData) {
251
+ if (!prData || typeof prData !== "object") return [];
252
+ const dedupe = (nums) => {
253
+ const seen = new Set();
254
+ const out = [];
255
+ for (const n of nums) {
256
+ if (Number.isInteger(n) && n > 0 && !seen.has(n)) {
257
+ seen.add(n);
258
+ out.push(n);
259
+ }
260
+ }
261
+ return out;
262
+ };
230
263
  const closing = Array.isArray(prData.closingIssuesReferences) ? prData.closingIssuesReferences : [];
231
- const closingNumbers = closing
232
- .map((entry) => Number(entry?.number))
233
- .filter((n) => Number.isInteger(n) && n > 0);
234
- if (closingNumbers.length === 1) {
235
- return closingNumbers[0];
264
+ const closingNumbers = dedupe(closing.map((entry) => Number(entry?.number)));
265
+ if (closingNumbers.length > 0) {
266
+ return closingNumbers;
236
267
  }
237
268
  const body = typeof prData.body === "string" ? prData.body : "";
238
- if (body.length === 0) return null;
269
+ if (body.length === 0) return [];
239
270
  const matches = body.match(/(?:closes|fixes|resolves)\s+#(\d+)/gi) || [];
240
- const bodyNumbers = matches
241
- .map((m) => Number((/(\d+)/.exec(m) || [])[1]))
242
- .filter((n) => Number.isInteger(n) && n > 0);
243
- if (bodyNumbers.length === 1) {
244
- return bodyNumbers[0];
245
- }
246
- return null;
271
+ return dedupe(matches.map((m) => Number((/(\d+)/.exec(m) || [])[1])));
247
272
  }
273
+
248
274
  async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand = "gh" } = {}) {
249
275
  const result = await runChild(
250
276
  ghCommand,
@@ -261,58 +287,118 @@ async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand =
261
287
  return null;
262
288
  }
263
289
  }
264
- async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh" } = {}) {
265
- const linkedIssue = resolveLinkedIssueFromPr(prData);
266
- if (linkedIssue === null) {
290
+ export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh" } = {}) {
291
+ const linkedIssues = resolveLinkedIssuesFromPr(prData);
292
+ if (linkedIssues.length === 0) {
267
293
  if (prDraft) {
268
294
  return {
269
295
  status: "missing",
270
296
  linkedIssue: null,
271
- reason: "Draft PR has no deterministically resolvable linked issue (no closingIssuesReferences, no unique Closes/Fixes/Resolves pattern in body); draft gate cannot verify a refinement artifact.",
297
+ linkedIssues: [],
298
+ reason: "Draft PR has no deterministically resolvable linked issue (no closingIssuesReferences and no Closes/Fixes/Resolves #n reference in body); draft gate cannot verify a refinement artifact.",
272
299
  finding: "missing_refinement_artifact",
273
300
  };
274
301
  }
275
302
  return {
276
303
  status: "unknown",
277
304
  linkedIssue: null,
278
- reason: "No deterministically resolvable linked issue (no closingIssuesReferences, no unique Closes/Fixes/Resolves pattern in body).",
305
+ linkedIssues: [],
306
+ reason: "No deterministically resolvable linked issue (no closingIssuesReferences and no Closes/Fixes/Resolves #n reference in body).",
279
307
  };
280
308
  }
309
+ const scopeLabel = linkedIssues.map((n) => `#${n}`).join(", ");
281
310
  if (!prDraft && !prClosed && !prMerged) {
282
311
  return {
283
312
  status: "unknown",
284
- linkedIssue,
285
- reason: `Linked issue #${linkedIssue} detected; refinement check is a draft-gate boundary and the PR is not draft, so the check is informational only and does not fetch the issue body.`,
313
+ linkedIssue: linkedIssues.length === 1 ? linkedIssues[0] : null,
314
+ linkedIssues,
315
+ reason: `Linked issue(s) ${scopeLabel} detected (${linkedIssues.length}); refinement check is a draft-gate boundary and the PR is not draft, so the check is informational only and does not fetch issue bodies.`,
316
+ };
317
+ }
318
+ const { detectIssueRefinementArtifact } = await import("@dev-loops/core/loop/issue-refinement-artifact");
319
+ // Fetch and evaluate every closing-referenced issue. An umbrella PR's scope
320
+ // is refined if AT LEAST ONE linked issue carries a refinement artifact.
321
+ const evaluated = [];
322
+ for (const issue of linkedIssues) {
323
+ const body = await fetchIssueBody({ repo, issue }, { env, ghCommand });
324
+ if (body === null) {
325
+ evaluated.push({ issue, artifact: null });
326
+ continue;
327
+ }
328
+ evaluated.push({ issue, artifact: detectIssueRefinementArtifact({ body, issueNumber: issue }) });
329
+ }
330
+ const refinedIssues = evaluated
331
+ .filter((e) => e.artifact && e.artifact.hasACs === true)
332
+ .map((e) => e.issue);
333
+ const firstPresent = evaluated.find((e) => e.artifact && e.artifact.hasACs === true);
334
+ const isUmbrella = linkedIssues.length > 1;
335
+
336
+ if (firstPresent) {
337
+ const a = firstPresent.artifact;
338
+ return {
339
+ status: "present",
340
+ linkedIssue: firstPresent.issue,
341
+ linkedIssues,
342
+ refinedIssues,
343
+ source: a.source,
344
+ acItems: a.acItems,
345
+ dodItems: a.dodItems,
346
+ sections: a.sections,
347
+ linkedDoc: a.linkedDoc,
348
+ reason: isUmbrella
349
+ ? `Refinement artifact present via linked issue #${firstPresent.issue} (umbrella PR closes ${scopeLabel}).`
350
+ : a.reason,
351
+ finding: a.finding,
352
+ _onlyEnforcedWhenDraft: prDraft === true,
286
353
  };
287
354
  }
288
- const body = await fetchIssueBody({ repo, issue: linkedIssue }, { env, ghCommand });
289
- if (body === null) {
355
+
356
+ // None of the linked issues carry a refinement artifact (or all bodies failed
357
+ // to fetch). Report against the first linked issue for single-value consumers.
358
+ // Note: `finding`/`missing` here is only enforced by the gate when the PR is
359
+ // draft (`_onlyEnforcedWhenDraft`); closed/merged PRs surface it informationally.
360
+ const firstEvaluated = evaluated[0];
361
+ const allFailed = evaluated.every((e) => e.artifact === null);
362
+ if (allFailed) {
363
+ // Preserve prior single-issue semantics: draft → missing, else unknown.
290
364
  if (prDraft) {
291
365
  return {
292
366
  status: "missing",
293
- linkedIssue,
294
- reason: `Failed to fetch body for linked issue #${linkedIssue}; draft gate cannot verify a refinement artifact, treating as missing.`,
367
+ linkedIssue: firstEvaluated.issue,
368
+ linkedIssues,
369
+ refinedIssues,
370
+ reason: `Failed to fetch body for linked issue(s) ${scopeLabel}; draft gate cannot verify a refinement artifact, treating as missing.`,
295
371
  finding: "missing_refinement_artifact",
296
372
  };
297
373
  }
298
374
  return {
299
375
  status: "unknown",
300
- linkedIssue,
301
- reason: `Failed to fetch body for linked issue #${linkedIssue}; refinement status is unknown.`,
376
+ linkedIssue: linkedIssues.length === 1 ? linkedIssues[0] : firstEvaluated.issue,
377
+ linkedIssues,
378
+ refinedIssues,
379
+ reason: `Failed to fetch body for linked issue(s) ${scopeLabel}; refinement status is unknown.`,
302
380
  };
303
381
  }
304
- const { detectIssueRefinementArtifact } = await import("@dev-loops/core/loop/issue-refinement-artifact");
305
- const artifact = detectIssueRefinementArtifact({ body, issueNumber: linkedIssue });
382
+ // Mixed branch: not allFailed, so at least one body fetched but none is
383
+ // refined. Report against the first successfully-fetched (non-null) issue —
384
+ // `evaluated[0]` may be a failed fetch: it still retains its `issue` field but
385
+ // has `artifact: null` (body fetch / artifact detection failed for that issue).
386
+ const firstFetched = evaluated.find((e) => e.artifact !== null);
387
+ const first = firstFetched.artifact;
306
388
  return {
307
- status: artifact.hasACs ? "present" : "missing",
308
- linkedIssue,
309
- source: artifact.source,
310
- acItems: artifact.acItems,
311
- dodItems: artifact.dodItems,
312
- sections: artifact.sections,
313
- linkedDoc: artifact.linkedDoc,
314
- reason: artifact.reason,
315
- finding: artifact.finding,
389
+ status: "missing",
390
+ linkedIssue: firstFetched.issue,
391
+ linkedIssues,
392
+ refinedIssues,
393
+ source: first.source,
394
+ acItems: first.acItems,
395
+ dodItems: first.dodItems,
396
+ sections: first.sections,
397
+ linkedDoc: first.linkedDoc,
398
+ reason: isUmbrella
399
+ ? `No linked issue (${scopeLabel}) carries a refinement artifact (ACs/DoD); draft gate cannot verify a refinement artifact.`
400
+ : first.reason,
401
+ finding: "missing_refinement_artifact",
316
402
  _onlyEnforcedWhenDraft: prDraft === true,
317
403
  };
318
404
  }
@@ -332,16 +418,6 @@ async function fetchLocalConflictFiles({ env = process.env, gitCommand = "git" }
332
418
  }
333
419
  return parseGitStatusConflictFiles(result.stdout);
334
420
  }
335
- async function loadRetrospectiveCheckpoint(repoRoot) {
336
- const checkpointPath = path.join(repoRoot, ".pi", "dev-loop-retrospective-checkpoint.json");
337
- try {
338
- const checkpointText = await readFile(checkpointPath, "utf8");
339
- const checkpoint = parseJsonText(checkpointText, { label: "retrospective checkpoint" });
340
- return checkpoint && typeof checkpoint === "object" ? checkpoint : null;
341
- } catch (error) {
342
- return null;
343
- }
344
- }
345
421
  export async function loadPrGateCoordinationContext(options, runtime = {}) {
346
422
  const prData = await fetchPrFactsWithSettledMergeable(options, runtime);
347
423
  const currentHeadSha = typeof prData?.headRefOid === "string" && prData.headRefOid.trim().length > 0
@@ -392,11 +468,22 @@ export async function loadPrGateCoordinationContext(options, runtime = {}) {
392
468
  // READY_TO_REREQUEST_REVIEW, dead-ending the loop at the round cap (#896). This
393
469
  // keeps the gate-coordination interpretation consistent with the standalone
394
470
  // detect-copilot-loop-state path and with request-copilot-review's cap logic.
395
- const interpreterRepoRoot = runtime.repoRoot ?? process.cwd();
471
+ const interpreterRepoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
396
472
  const interpreterConfigResult = await loadDevLoopConfig({ repoRoot: interpreterRepoRoot });
397
- const interpreterRefinementConfig = (Array.isArray(interpreterConfigResult.errors) && interpreterConfigResult.errors.length > 0)
473
+ const interpreterConfigHasErrors = Array.isArray(interpreterConfigResult.errors) && interpreterConfigResult.errors.length > 0;
474
+ const interpreterRefinementConfig = interpreterConfigHasErrors
398
475
  ? resolveRefinement({ version: 1 })
399
476
  : resolveRefinement(interpreterConfigResult.config ?? { version: 1 });
477
+ if (options.lightweight) {
478
+ // Compose (not replace) the round cap for light-dispatched PRs (#1210):
479
+ // min(lightMode.maxCopilotRounds ?? 1, refinement.maxCopilotRounds), so
480
+ // maxCopilotRounds: 0 still disables Copilot rounds everywhere. Shared with
481
+ // the maxCopilotRounds resolution below (#1126 requires the two to agree).
482
+ interpreterRefinementConfig.maxCopilotRounds = resolveEffectiveCopilotRoundCap(
483
+ interpreterConfigHasErrors ? { version: 1 } : (interpreterConfigResult.config ?? { version: 1 }),
484
+ { lightweight: true },
485
+ );
486
+ }
400
487
  const interpretation = interpretLoopState(snapshot, interpreterRefinementConfig);
401
488
  const disposition = summarizeLoopInterpretation(interpretation, interpreterRefinementConfig);
402
489
  const mergeStateStatus = typeof prData?.mergeStateStatus === "string" && prData.mergeStateStatus.trim().length > 0
@@ -446,15 +533,37 @@ async function fetchCopilotEverFormallyRequested({ repo, pr }, { env = process.e
446
533
 
447
534
  export async function detectPrGateCoordinationState(options, runtime = {}) {
448
535
  const context = await loadPrGateCoordinationContext(options, runtime);
449
- const repoRoot = runtime.repoRoot ?? process.cwd();
536
+ const repoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
450
537
  const configLoadResult = await loadDevLoopConfig({ repoRoot });
451
538
  const hasConfigErrors = Array.isArray(configLoadResult.errors) && configLoadResult.errors.length > 0;
452
539
  const config = hasConfigErrors ? {} : (configLoadResult.config ?? {});
453
540
  const draftGateConfig = resolveGateConfig(config, "draft");
454
- const maxCopilotRounds = resolveRefinementConfig(config, "maxCopilotRounds");
455
- const requireRetrospectiveGate = resolveWorkflowConfig(config, "requireRetrospectiveGate");
456
- const requireRetrospectiveInternalTooling = resolveWorkflowConfig(config, "requireRetrospectiveInternalTooling");
457
- const retrospectiveCheckpoint = await loadRetrospectiveCheckpoint(repoRoot);
541
+ // Shared with interpreterRefinementConfig.maxCopilotRounds in
542
+ // loadPrGateCoordinationContext (#1126: the two must never disagree at the
543
+ // cap boundary) — the same lightweight composition (#1210) is applied here.
544
+ const maxCopilotRounds = options.lightweight
545
+ ? resolveEffectiveCopilotRoundCap(config, { lightweight: true })
546
+ : resolveRefinementConfig(config, "maxCopilotRounds");
547
+ // Shared with interpretLoopState (consumed by copilot-pr-handoff.mjs) and
548
+ // evaluatePrGateCoordination — the single source of truth for "is the
549
+ // Copilot round cap reached" so this detector cannot disagree with the
550
+ // handoff at the cap boundary (#1126).
551
+ const roundCapReached = isCopilotRoundCapReached({
552
+ copilotReviewRoundCount: context.snapshot?.copilotReviewRoundCount,
553
+ maxCopilotRounds,
554
+ });
555
+ const postConvergenceSignificantChange = await detectPostConvergenceSignificantChange(
556
+ {
557
+ repo: context.repo,
558
+ pr: context.pr,
559
+ currentHeadSha: context.currentHeadSha,
560
+ reviews: context.prData?.reviews,
561
+ changedFiles: context.prData?.files,
562
+ roundCapReached: roundCapReached && context.interpretation?.roundCapCleanEligible === true,
563
+ regularCopilotRounds: (context.snapshot?.copilotReviewRoundCount ?? 0) > 0,
564
+ },
565
+ runtime,
566
+ );
458
567
  const result = evaluatePrGateCoordination({
459
568
  repo: context.repo,
460
569
  pr: context.pr,
@@ -475,15 +584,17 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
475
584
  copilotReviewRoundCount: context.snapshot?.copilotReviewRoundCount ?? 0,
476
585
  maxCopilotRounds,
477
586
  sameHeadCleanConverged: context.interpretation.sameHeadCleanConverged,
587
+ // Independent gate-ENTRY re-check (#1190): fed alongside (not derived from)
588
+ // sameHeadCleanConverged, so an outstanding request on the current head refuses
589
+ // RUN_PRE_APPROVAL_GATE even if sameHeadCleanConverged were somehow stale/wrong.
590
+ copilotReviewRequestStatus: context.snapshot?.copilotReviewRequestStatus ?? "none",
478
591
  draftGateRequireCi: draftGateConfig.requireCi,
479
- requireRetrospectiveGate,
480
- requireRetrospectiveInternalTooling,
481
- retrospectiveCheckpoint,
482
592
  draftGate: context.gateEvidence.draftGate,
483
593
  draftGateMarker: context.gateEvidence.draftGateMarker,
484
594
  preApprovalGate: context.gateEvidence.preApprovalGate,
485
595
  preApprovalGateMarker: context.gateEvidence.preApprovalGateMarker,
486
596
  refinementArtifact: context.refinementArtifact,
597
+ postConvergenceSignificantChange,
487
598
  });
488
599
  // Copilot review request guard (#613): When Copilot has reviewed the PR
489
600
  // but no formal review request was made, block pre-approval gate entry.
@@ -495,9 +606,6 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
495
606
  PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
496
607
  PR_CHECKPOINT.FINAL_APPROVAL_READY,
497
608
  ]);
498
- const roundCapReached = maxCopilotRounds !== null
499
- && typeof (context.snapshot?.copilotReviewRoundCount) === "number"
500
- && context.snapshot?.copilotReviewRoundCount >= maxCopilotRounds;
501
609
  const sameHeadCleanConverged = context.interpretation?.sameHeadCleanConverged ?? false;
502
610
  // Round-cap clean fallback (#896): the interpreter resolved a clean post-cap head
503
611
  // (zero unresolved threads + green CI) that Copilot will not re-review. The formal
@@ -505,7 +613,13 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
505
613
  const roundCapCleanFallback = context.interpretation?.roundCapCleanEligible ?? false;
506
614
  const copilotReviewEverFormallyRequested = copilotReviewRequestStatus === "none"
507
615
  && guardBoundaries.has(result.gateBoundary)
508
- && !(roundCapReached && (sameHeadCleanConverged || roundCapCleanFallback))
616
+ // cap-0 disables the Copilot gate, so shouldGuardCopilotReviewRequest always
617
+ // returns false here — skip the timeline fetch it would never need (#1126).
618
+ // (Restores the suppression the roundCapReached predicate swap dropped.)
619
+ && maxCopilotRounds !== 0
620
+ && !(roundCapReached
621
+ && (sameHeadCleanConverged || roundCapCleanFallback)
622
+ && !postConvergenceSignificantChange)
509
623
  ? await fetchCopilotEverFormallyRequested(
510
624
  { repo: context.repo, pr: context.pr },
511
625
  runtime,
@@ -518,6 +632,7 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
518
632
  maxCopilotRounds,
519
633
  sameHeadCleanConverged,
520
634
  roundCapCleanFallback,
635
+ postConvergenceSignificantChange,
521
636
  gateBoundary: result.gateBoundary,
522
637
  })) {
523
638
  result.gateBoundary = PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW;
@@ -590,7 +705,7 @@ async function main() {
590
705
  }
591
706
  try {
592
707
  const result = await detectPrGateCoordinationState(options);
593
- process.stdout.write(`${JSON.stringify(result)}\n`);
708
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
594
709
  } catch (error) {
595
710
  process.stderr.write(`${formatCliError(error)}\n`);
596
711
  process.exitCode = 1;
@@ -8,6 +8,7 @@ import {
8
8
  interpretReviewerLoopState,
9
9
  normalizeReviewerSnapshot,
10
10
  } from "@dev-loops/core/loop/reviewer-loop-state";
11
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
11
12
  const HELP = `Usage: detect-reviewer-loop-state.mjs [--input <path> | --repo <owner/name> --pr <number>] [--review-requested <true|false>] [--local-state <path>]
12
13
  Detect reviewer loop state for a pull request.
13
14
  Modes:
@@ -17,9 +18,13 @@ Options (auto-detect mode only):
17
18
  --review-requested <bool> Override review-requested detection (true/false)
18
19
  --local-state <path> Path to local state file for snapshot merging
19
20
  Reviewer scope is auto-resolved from PR requested reviewers.
21
+
22
+ ${JQ_OUTPUT_USAGE}
23
+
20
24
  Exit codes:
21
25
  0 Success
22
26
  1 Error
27
+ 2 Invalid --jq filter
23
28
  `;
24
29
  function parseBool(value, flag) {
25
30
  if (value === "true") return true;
@@ -36,6 +41,7 @@ export function parseDetectReviewerCliArgs(argv) {
36
41
  pr: { type: "string" },
37
42
  "review-requested": { type: "string" },
38
43
  "local-state": { type: "string" },
44
+ ...JQ_OUTPUT_PARSE_OPTIONS,
39
45
  },
40
46
  allowPositionals: true,
41
47
  strict: false,
@@ -83,6 +89,7 @@ export function parseDetectReviewerCliArgs(argv) {
83
89
  options.localStatePath = requireTokenValue(token);
84
90
  continue;
85
91
  }
92
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t))) continue;
86
93
  throw new Error(`Unknown argument: ${token.rawName}`);
87
94
  }
88
95
  if (options.inputPath !== undefined) {
@@ -231,7 +238,7 @@ export async function autoDetectReviewerSnapshot(
231
238
  }
232
239
  export async function runCli(
233
240
  argv = process.argv.slice(2),
234
- { stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
241
+ { stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
235
242
  ) {
236
243
  const options = parseDetectReviewerCliArgs(argv);
237
244
  if (options.help) { stdout.write(HELP); return; }
@@ -243,7 +250,10 @@ export async function runCli(
243
250
  snapshot = await autoDetectReviewerSnapshot(options, { env, ghCommand });
244
251
  }
245
252
  const interpretation = interpretReviewerLoopState(snapshot);
246
- stdout.write(`${JSON.stringify({ ok: true, snapshot, state: interpretation.state, allowedTransitions: interpretation.allowedTransitions, nextAction: interpretation.nextAction })}\n`);
253
+ process.exitCode = emitResult(
254
+ { ok: true, snapshot, state: interpretation.state, allowedTransitions: interpretation.allowedTransitions, nextAction: interpretation.nextAction },
255
+ { jq: options.jq, silent: options.silent, stdout, stderr },
256
+ );
247
257
  }
248
258
  if (isDirectCliRun(import.meta.url)) {
249
259
  runCli().catch((error) => { process.stderr.write(`${formatCliError(error)}\n`); process.exitCode = 1; });
@@ -3,6 +3,7 @@ import process from "node:process";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { parseArgs } from "node:util";
5
5
  import { interpretTrackerLoopState } from "@dev-loops/core/loop/tracker-first-loop-state";
6
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
6
7
 
7
8
  function showHelp() {
8
9
  process.stdout.write(`Usage: detect-tracker-first-loop-state.mjs --repo <owner/name> --issue <number>
@@ -11,9 +12,13 @@ Options:
11
12
  --repo <owner/name> GitHub repository slug
12
13
  --issue <number> GitHub issue number
13
14
  --help, -h Show this help
15
+
16
+ ${JQ_OUTPUT_USAGE}
17
+
14
18
  Exit codes:
15
19
  0 Success
16
20
  1 Error
21
+ 2 Invalid --jq filter
17
22
  `);
18
23
  process.exit(0);
19
24
  }
@@ -25,6 +30,7 @@ function parseCliArgs(argv) {
25
30
  repo: { type: "string" },
26
31
  issue: { type: "string" },
27
32
  help: { type: "boolean", short: "h" },
33
+ ...JQ_OUTPUT_PARSE_OPTIONS,
28
34
  },
29
35
  allowPositionals: true,
30
36
  strict: false,
@@ -46,7 +52,9 @@ function parseCliArgs(argv) {
46
52
  }
47
53
  if (token.name === "issue") {
48
54
  opts.issue = token.value ?? null;
55
+ continue;
49
56
  }
57
+ if (matchJqOutputToken(token, opts)) continue;
50
58
  }
51
59
  }
52
60
  return opts;
@@ -90,7 +98,7 @@ async function main() {
90
98
  return;
91
99
  }
92
100
  const result = interpretTrackerLoopState({ trackerState: rawState, prContext });
93
- process.stdout.write(JSON.stringify(result) + "\n");
101
+ process.exitCode = emitResult(result, { jq: rawOpts.jq, silent: rawOpts.silent });
94
102
  }
95
103
  const isDirectRun =
96
104
  process.argv[1] && process.argv[1].includes("detect-tracker-first-loop-state.mjs");
@@ -9,6 +9,7 @@ import {
9
9
  interpretTrackerPrState,
10
10
  normalizeTrackerPrSnapshot,
11
11
  } from "@dev-loops/core/loop/tracker-pr-state";
12
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
12
13
  const USAGE = `Usage:
13
14
  detect-tracker-pr-state.mjs --input <path>
14
15
  Interpret a pre-built tracker-PR snapshot JSON and emit the current lifecycle
@@ -47,9 +48,11 @@ Output (stdout, JSON):
47
48
  Error output (stderr, JSON):
48
49
  Argument/usage errors: { "ok": false, "error": "...", "usage": "..." }
49
50
  Runtime failures: { "ok": false, "error": "..." }
51
+ ${JQ_OUTPUT_USAGE}
50
52
  Exit codes:
51
53
  0 Success
52
- 1 Argument error or runtime failure`.trim();
54
+ 1 Argument error or runtime failure
55
+ 2 Invalid --jq filter`.trim();
53
56
  const parseError = buildParseError(USAGE);
54
57
  export function parseDetectTrackerPrCliArgs(argv) {
55
58
  const options = {
@@ -61,6 +64,7 @@ export function parseDetectTrackerPrCliArgs(argv) {
61
64
  options: {
62
65
  help: { type: "boolean", short: "h" },
63
66
  input: { type: "string" },
67
+ ...JQ_OUTPUT_PARSE_OPTIONS,
64
68
  },
65
69
  allowPositionals: true,
66
70
  strict: false,
@@ -81,6 +85,7 @@ export function parseDetectTrackerPrCliArgs(argv) {
81
85
  options.inputPath = requireTokenValue(token, parseError);
82
86
  continue;
83
87
  }
88
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
84
89
  throw parseError(`Unknown argument: ${token.rawName}`);
85
90
  }
86
91
  if (options.inputPath === undefined) {
@@ -92,6 +97,7 @@ export async function runCli(
92
97
  argv = process.argv.slice(2),
93
98
  {
94
99
  stdout = process.stdout,
100
+ stderr = process.stderr,
95
101
  } = {},
96
102
  ) {
97
103
  const options = parseDetectTrackerPrCliArgs(argv);
@@ -103,8 +109,9 @@ export async function runCli(
103
109
  const raw = parseJsonText(text);
104
110
  const snapshot = normalizeTrackerPrSnapshot(raw);
105
111
  const { state, allowedTransitions, nextAction, reverseSyncAction } = interpretTrackerPrState(snapshot);
106
- stdout.write(
107
- `${JSON.stringify({ ok: true, snapshot, state, allowedTransitions, nextAction, reverseSyncAction })}\n`,
112
+ process.exitCode = emitResult(
113
+ { ok: true, snapshot, state, allowedTransitions, nextAction, reverseSyncAction },
114
+ { jq: options.jq, silent: options.silent, stdout, stderr },
108
115
  );
109
116
  }
110
117
  const isDirectRun =
@@ -27,6 +27,7 @@ import { parseArgs } from "node:util";
27
27
  import { resolveWorktreePath } from "@dev-loops/core/loop/handoff-envelope";
28
28
  import { provisionWorktree } from "./provision-worktree.mjs";
29
29
  import { canonicalize } from "./_worktree-path.mjs";
30
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
30
31
 
31
32
  const USAGE = `Usage:
32
33
  ensure-worktree.mjs --repo-root <p> (--issue <n> | --pr <n>) [--branch <name>] [--base <ref>]
@@ -43,7 +44,9 @@ Optional:
43
44
  -h, --help Show this help.
44
45
  Output (stdout, JSON):
45
46
  { "ok": true, "path": <p>, "created": bool, "reused": bool,
46
- "provision": { "actions": [...], "summary": {...} } }`.trim();
47
+ "provision": { "actions": [...], "summary": {...} } }
48
+
49
+ ${JQ_OUTPUT_USAGE}`.trim();
47
50
 
48
51
  const parseError = buildParseError(USAGE);
49
52
 
@@ -71,6 +74,7 @@ export function parseEnsureWorktreeCliArgs(argv) {
71
74
  pr: { type: "string" },
72
75
  branch: { type: "string" },
73
76
  base: { type: "string" },
77
+ ...JQ_OUTPUT_PARSE_OPTIONS,
74
78
  },
75
79
  allowPositionals: true,
76
80
  strict: false,
@@ -103,6 +107,7 @@ export function parseEnsureWorktreeCliArgs(argv) {
103
107
  options.base = requireTokenValue(token, parseError, { flagPattern: /^-/u });
104
108
  continue;
105
109
  }
110
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
106
111
  throw parseError(`Unknown argument: ${token.rawName}`);
107
112
  }
108
113
  if (options.help) return options;
@@ -201,14 +206,14 @@ export async function ensureWorktree(
201
206
  return { ok: true, path: target, created: true, reused: false, provision: summary };
202
207
  }
203
208
 
204
- export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
209
+ export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout, stderr = process.stderr } = {}) {
205
210
  const options = parseEnsureWorktreeCliArgs(argv);
206
211
  if (options.help) {
207
212
  stdout.write(`${USAGE}\n`);
208
213
  return;
209
214
  }
210
215
  const result = await ensureWorktree(options);
211
- stdout.write(`${JSON.stringify(result)}\n`);
216
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
212
217
  }
213
218
 
214
219
  if (isDirectCliRun(import.meta.url)) {