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
@@ -8,6 +8,7 @@ import {
8
8
  detectIssueRefinementArtifact,
9
9
  REFINEMENT_SOURCE,
10
10
  } from "@dev-loops/core/loop/issue-refinement-artifact";
11
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
11
12
  const USAGE = `Usage:
12
13
  detect-issue-refinement-artifact.mjs --repo <owner/name> --issue <number>
13
14
  detect-issue-refinement-artifact.mjs --input <path>
@@ -31,7 +32,8 @@ Success output (stdout, JSON):
31
32
  "reason": "..."
32
33
  }
33
34
  Error output (stderr, JSON):
34
- { "ok": false, "error": "...", "usage": "..." }`.trim();
35
+ { "ok": false, "error": "...", "usage": "..." }
36
+ ${JQ_OUTPUT_USAGE}`.trim();
35
37
  const parseError = buildParseError(USAGE);
36
38
  export function parseDetectIssueRefinementArtifactCliArgs(argv) {
37
39
  const options = {
@@ -47,6 +49,7 @@ export function parseDetectIssueRefinementArtifactCliArgs(argv) {
47
49
  repo: { type: "string" },
48
50
  issue: { type: "string" },
49
51
  input: { type: "string" },
52
+ ...JQ_OUTPUT_PARSE_OPTIONS,
50
53
  },
51
54
  allowPositionals: true,
52
55
  strict: false,
@@ -79,6 +82,7 @@ export function parseDetectIssueRefinementArtifactCliArgs(argv) {
79
82
  options.input = requireTokenValue(token, parseError).trim();
80
83
  continue;
81
84
  }
85
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
82
86
  throw parseError(`Unknown argument: ${token.rawName}`);
83
87
  }
84
88
  const hasInput = typeof options.input === "string" && options.input.length > 0;
@@ -165,8 +169,7 @@ export async function runCli(
165
169
  }
166
170
  try {
167
171
  const result = await detectIssueRefinementArtifactFromOptions(options, { env, ghCommand });
168
- stdout.write(`${JSON.stringify(result)}\n`);
169
- return 0;
172
+ return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
170
173
  } catch (error) {
171
174
  stderr.write(`${formatCliError(error)}\n`);
172
175
  return 1;
@@ -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,
@@ -14,12 +13,16 @@ import {
14
13
  import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
15
14
  import { loadDevLoopConfig, 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";
19
+ import { UI_E2E_CHECK_NAMES } from "@dev-loops/core/loop/ui-e2e-scoping";
20
20
  import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
21
+ import { detectPostConvergenceSignificantChange } from "./_post-convergence-change.mjs";
21
22
  import { detectCheckpointEvidence } from "../github/detect-checkpoint-evidence.mjs";
23
+ import { resolveRepoRoot } from "./_repo-root-resolver.mjs";
22
24
  import { parseArgs } from "node:util";
25
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
23
26
  const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
24
27
  const USAGE = `Usage: detect-pr-gate-coordination-state.mjs --repo <owner/name> --pr <number>
25
28
  Determine which PR gate/transition is legal next for a pull request.
@@ -66,9 +69,11 @@ Output (stdout, JSON):
66
69
  Error output (stderr, JSON):
67
70
  { "ok": false, "error": "...", "usage": "..." }
68
71
  { "ok": false, "error": "..." }
72
+ ${JQ_OUTPUT_USAGE}
69
73
  Exit codes:
70
74
  0 Success
71
- 1 Argument error or gh/runtime failure`.trim();
75
+ 1 Argument error or gh/runtime failure
76
+ 2 Invalid --jq filter`.trim();
72
77
  const parseError = buildParseError(USAGE);
73
78
  export function parseDetectPrGateCoordinationCliArgs(argv) {
74
79
  const options = {
@@ -82,6 +87,7 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
82
87
  help: { type: "boolean", short: "h" },
83
88
  repo: { type: "string" },
84
89
  pr: { type: "string" },
90
+ ...JQ_OUTPUT_PARSE_OPTIONS,
85
91
  },
86
92
  allowPositionals: true,
87
93
  strict: false,
@@ -106,6 +112,7 @@ export function parseDetectPrGateCoordinationCliArgs(argv) {
106
112
  options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
107
113
  continue;
108
114
  }
115
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
109
116
  throw parseError(`Unknown argument: ${token.rawName}`);
110
117
  }
111
118
  if (options.repo === undefined || options.pr === undefined) {
@@ -163,7 +170,7 @@ async function fetchRequestedReviewers({ repo, pr }, { env = process.env, ghComm
163
170
  async function fetchPrFacts({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
164
171
  const result = await runChild(
165
172
  ghCommand,
166
- ["pr", "view", String(pr), "--repo", repo, "--json", "number,state,isDraft,headRefOid,mergeable,mergeStateStatus,body,title,closingIssuesReferences,reviews,statusCheckRollup"],
173
+ ["pr", "view", String(pr), "--repo", repo, "--json", "number,state,isDraft,headRefOid,mergeable,mergeStateStatus,body,title,closingIssuesReferences,reviews,statusCheckRollup,files"],
167
174
  env,
168
175
  );
169
176
  if (result.code !== 0) {
@@ -198,26 +205,60 @@ export async function fetchPrFactsWithSettledMergeable(
198
205
  }
199
206
  return prData;
200
207
  }
201
- export function resolveLinkedIssueFromPr(prData) {
202
- if (!prData || typeof prData !== "object") return null;
208
+ // Changed-file paths from `gh pr view --json files` (issue #976). Feeds the
209
+ // path-triggered UI e2e scoping precondition in the evaluator.
210
+ export function extractChangedFiles(prData) {
211
+ const files = Array.isArray(prData?.files) ? prData.files : [];
212
+ return files
213
+ .map((entry) => (typeof entry?.path === "string" ? entry.path : null))
214
+ .filter((p) => typeof p === "string" && p.length > 0);
215
+ }
216
+
217
+ // Whether the shared UI e2e suite passed for this head, read deterministically
218
+ // from the statusCheckRollup: every UI e2e check that is present must be
219
+ // SUCCESS. Returns null when no UI e2e check is present in the rollup (unknown
220
+ // → the evaluator fails closed), false if any present UI e2e check is not a
221
+ // success, true if all present ones succeeded.
222
+ export function deriveUiE2ePassed(prData, checkNames = UI_E2E_CHECK_NAMES) {
223
+ const rollup = Array.isArray(prData?.statusCheckRollup) ? prData.statusCheckRollup : [];
224
+ const wanted = new Set(checkNames);
225
+ const present = rollup.filter((entry) => wanted.has(entry?.name) || wanted.has(entry?.context));
226
+ if (present.length === 0) return null;
227
+ return present.every((entry) => {
228
+ const conclusion = String(entry?.conclusion ?? "").toUpperCase();
229
+ const state = String(entry?.state ?? "").toUpperCase();
230
+ // SKIPPED = "not applicable to this run" (e.g. viewer-smoke when no viewer files changed) — not a failure.
231
+ return conclusion === "SUCCESS" || conclusion === "SKIPPED" || state === "SUCCESS" || state === "SKIPPED";
232
+ });
233
+ }
234
+
235
+ // Ordered, de-duplicated list of ALL closing-referenced issue numbers for a PR.
236
+ // Umbrella PRs legitimately close multiple issues (#1052), so the refinement
237
+ // guard resolves against every one of them, not just a unique single ref.
238
+ export function resolveLinkedIssuesFromPr(prData) {
239
+ if (!prData || typeof prData !== "object") return [];
240
+ const dedupe = (nums) => {
241
+ const seen = new Set();
242
+ const out = [];
243
+ for (const n of nums) {
244
+ if (Number.isInteger(n) && n > 0 && !seen.has(n)) {
245
+ seen.add(n);
246
+ out.push(n);
247
+ }
248
+ }
249
+ return out;
250
+ };
203
251
  const closing = Array.isArray(prData.closingIssuesReferences) ? prData.closingIssuesReferences : [];
204
- const closingNumbers = closing
205
- .map((entry) => Number(entry?.number))
206
- .filter((n) => Number.isInteger(n) && n > 0);
207
- if (closingNumbers.length === 1) {
208
- return closingNumbers[0];
252
+ const closingNumbers = dedupe(closing.map((entry) => Number(entry?.number)));
253
+ if (closingNumbers.length > 0) {
254
+ return closingNumbers;
209
255
  }
210
256
  const body = typeof prData.body === "string" ? prData.body : "";
211
- if (body.length === 0) return null;
257
+ if (body.length === 0) return [];
212
258
  const matches = body.match(/(?:closes|fixes|resolves)\s+#(\d+)/gi) || [];
213
- const bodyNumbers = matches
214
- .map((m) => Number((/(\d+)/.exec(m) || [])[1]))
215
- .filter((n) => Number.isInteger(n) && n > 0);
216
- if (bodyNumbers.length === 1) {
217
- return bodyNumbers[0];
218
- }
219
- return null;
259
+ return dedupe(matches.map((m) => Number((/(\d+)/.exec(m) || [])[1])));
220
260
  }
261
+
221
262
  async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand = "gh" } = {}) {
222
263
  const result = await runChild(
223
264
  ghCommand,
@@ -234,58 +275,118 @@ async function fetchIssueBody({ repo, issue }, { env = process.env, ghCommand =
234
275
  return null;
235
276
  }
236
277
  }
237
- async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh" } = {}) {
238
- const linkedIssue = resolveLinkedIssueFromPr(prData);
239
- if (linkedIssue === null) {
278
+ export async function loadRefinementArtifact({ repo, prData, prDraft, prClosed, prMerged }, { env = process.env, ghCommand = "gh" } = {}) {
279
+ const linkedIssues = resolveLinkedIssuesFromPr(prData);
280
+ if (linkedIssues.length === 0) {
240
281
  if (prDraft) {
241
282
  return {
242
283
  status: "missing",
243
284
  linkedIssue: null,
244
- 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.",
285
+ linkedIssues: [],
286
+ 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.",
245
287
  finding: "missing_refinement_artifact",
246
288
  };
247
289
  }
248
290
  return {
249
291
  status: "unknown",
250
292
  linkedIssue: null,
251
- reason: "No deterministically resolvable linked issue (no closingIssuesReferences, no unique Closes/Fixes/Resolves pattern in body).",
293
+ linkedIssues: [],
294
+ reason: "No deterministically resolvable linked issue (no closingIssuesReferences and no Closes/Fixes/Resolves #n reference in body).",
252
295
  };
253
296
  }
297
+ const scopeLabel = linkedIssues.map((n) => `#${n}`).join(", ");
254
298
  if (!prDraft && !prClosed && !prMerged) {
255
299
  return {
256
300
  status: "unknown",
257
- linkedIssue,
258
- 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.`,
301
+ linkedIssue: linkedIssues.length === 1 ? linkedIssues[0] : null,
302
+ linkedIssues,
303
+ 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.`,
259
304
  };
260
305
  }
261
- const body = await fetchIssueBody({ repo, issue: linkedIssue }, { env, ghCommand });
262
- if (body === null) {
306
+ const { detectIssueRefinementArtifact } = await import("@dev-loops/core/loop/issue-refinement-artifact");
307
+ // Fetch and evaluate every closing-referenced issue. An umbrella PR's scope
308
+ // is refined if AT LEAST ONE linked issue carries a refinement artifact.
309
+ const evaluated = [];
310
+ for (const issue of linkedIssues) {
311
+ const body = await fetchIssueBody({ repo, issue }, { env, ghCommand });
312
+ if (body === null) {
313
+ evaluated.push({ issue, artifact: null });
314
+ continue;
315
+ }
316
+ evaluated.push({ issue, artifact: detectIssueRefinementArtifact({ body, issueNumber: issue }) });
317
+ }
318
+ const refinedIssues = evaluated
319
+ .filter((e) => e.artifact && e.artifact.hasACs === true)
320
+ .map((e) => e.issue);
321
+ const firstPresent = evaluated.find((e) => e.artifact && e.artifact.hasACs === true);
322
+ const isUmbrella = linkedIssues.length > 1;
323
+
324
+ if (firstPresent) {
325
+ const a = firstPresent.artifact;
326
+ return {
327
+ status: "present",
328
+ linkedIssue: firstPresent.issue,
329
+ linkedIssues,
330
+ refinedIssues,
331
+ source: a.source,
332
+ acItems: a.acItems,
333
+ dodItems: a.dodItems,
334
+ sections: a.sections,
335
+ linkedDoc: a.linkedDoc,
336
+ reason: isUmbrella
337
+ ? `Refinement artifact present via linked issue #${firstPresent.issue} (umbrella PR closes ${scopeLabel}).`
338
+ : a.reason,
339
+ finding: a.finding,
340
+ _onlyEnforcedWhenDraft: prDraft === true,
341
+ };
342
+ }
343
+
344
+ // None of the linked issues carry a refinement artifact (or all bodies failed
345
+ // to fetch). Report against the first linked issue for single-value consumers.
346
+ // Note: `finding`/`missing` here is only enforced by the gate when the PR is
347
+ // draft (`_onlyEnforcedWhenDraft`); closed/merged PRs surface it informationally.
348
+ const firstEvaluated = evaluated[0];
349
+ const allFailed = evaluated.every((e) => e.artifact === null);
350
+ if (allFailed) {
351
+ // Preserve prior single-issue semantics: draft → missing, else unknown.
263
352
  if (prDraft) {
264
353
  return {
265
354
  status: "missing",
266
- linkedIssue,
267
- reason: `Failed to fetch body for linked issue #${linkedIssue}; draft gate cannot verify a refinement artifact, treating as missing.`,
355
+ linkedIssue: firstEvaluated.issue,
356
+ linkedIssues,
357
+ refinedIssues,
358
+ reason: `Failed to fetch body for linked issue(s) ${scopeLabel}; draft gate cannot verify a refinement artifact, treating as missing.`,
268
359
  finding: "missing_refinement_artifact",
269
360
  };
270
361
  }
271
362
  return {
272
363
  status: "unknown",
273
- linkedIssue,
274
- reason: `Failed to fetch body for linked issue #${linkedIssue}; refinement status is unknown.`,
364
+ linkedIssue: linkedIssues.length === 1 ? linkedIssues[0] : firstEvaluated.issue,
365
+ linkedIssues,
366
+ refinedIssues,
367
+ reason: `Failed to fetch body for linked issue(s) ${scopeLabel}; refinement status is unknown.`,
275
368
  };
276
369
  }
277
- const { detectIssueRefinementArtifact } = await import("@dev-loops/core/loop/issue-refinement-artifact");
278
- const artifact = detectIssueRefinementArtifact({ body, issueNumber: linkedIssue });
370
+ // Mixed branch: not allFailed, so at least one body fetched but none is
371
+ // refined. Report against the first successfully-fetched (non-null) issue —
372
+ // `evaluated[0]` may be a failed fetch: it still retains its `issue` field but
373
+ // has `artifact: null` (body fetch / artifact detection failed for that issue).
374
+ const firstFetched = evaluated.find((e) => e.artifact !== null);
375
+ const first = firstFetched.artifact;
279
376
  return {
280
- status: artifact.hasACs ? "present" : "missing",
281
- linkedIssue,
282
- source: artifact.source,
283
- acItems: artifact.acItems,
284
- dodItems: artifact.dodItems,
285
- sections: artifact.sections,
286
- linkedDoc: artifact.linkedDoc,
287
- reason: artifact.reason,
288
- finding: artifact.finding,
377
+ status: "missing",
378
+ linkedIssue: firstFetched.issue,
379
+ linkedIssues,
380
+ refinedIssues,
381
+ source: first.source,
382
+ acItems: first.acItems,
383
+ dodItems: first.dodItems,
384
+ sections: first.sections,
385
+ linkedDoc: first.linkedDoc,
386
+ reason: isUmbrella
387
+ ? `No linked issue (${scopeLabel}) carries a refinement artifact (ACs/DoD); draft gate cannot verify a refinement artifact.`
388
+ : first.reason,
389
+ finding: "missing_refinement_artifact",
289
390
  _onlyEnforcedWhenDraft: prDraft === true,
290
391
  };
291
392
  }
@@ -305,16 +406,6 @@ async function fetchLocalConflictFiles({ env = process.env, gitCommand = "git" }
305
406
  }
306
407
  return parseGitStatusConflictFiles(result.stdout);
307
408
  }
308
- async function loadRetrospectiveCheckpoint(repoRoot) {
309
- const checkpointPath = path.join(repoRoot, ".pi", "dev-loop-retrospective-checkpoint.json");
310
- try {
311
- const checkpointText = await readFile(checkpointPath, "utf8");
312
- const checkpoint = parseJsonText(checkpointText, { label: "retrospective checkpoint" });
313
- return checkpoint && typeof checkpoint === "object" ? checkpoint : null;
314
- } catch (error) {
315
- return null;
316
- }
317
- }
318
409
  export async function loadPrGateCoordinationContext(options, runtime = {}) {
319
410
  const prData = await fetchPrFactsWithSettledMergeable(options, runtime);
320
411
  const currentHeadSha = typeof prData?.headRefOid === "string" && prData.headRefOid.trim().length > 0
@@ -365,7 +456,7 @@ export async function loadPrGateCoordinationContext(options, runtime = {}) {
365
456
  // READY_TO_REREQUEST_REVIEW, dead-ending the loop at the round cap (#896). This
366
457
  // keeps the gate-coordination interpretation consistent with the standalone
367
458
  // detect-copilot-loop-state path and with request-copilot-review's cap logic.
368
- const interpreterRepoRoot = runtime.repoRoot ?? process.cwd();
459
+ const interpreterRepoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
369
460
  const interpreterConfigResult = await loadDevLoopConfig({ repoRoot: interpreterRepoRoot });
370
461
  const interpreterRefinementConfig = (Array.isArray(interpreterConfigResult.errors) && interpreterConfigResult.errors.length > 0)
371
462
  ? resolveRefinement({ version: 1 })
@@ -419,15 +510,32 @@ async function fetchCopilotEverFormallyRequested({ repo, pr }, { env = process.e
419
510
 
420
511
  export async function detectPrGateCoordinationState(options, runtime = {}) {
421
512
  const context = await loadPrGateCoordinationContext(options, runtime);
422
- const repoRoot = runtime.repoRoot ?? process.cwd();
513
+ const repoRoot = runtime.repoRoot ?? resolveRepoRoot(process.cwd());
423
514
  const configLoadResult = await loadDevLoopConfig({ repoRoot });
424
515
  const hasConfigErrors = Array.isArray(configLoadResult.errors) && configLoadResult.errors.length > 0;
425
516
  const config = hasConfigErrors ? {} : (configLoadResult.config ?? {});
426
517
  const draftGateConfig = resolveGateConfig(config, "draft");
427
518
  const maxCopilotRounds = resolveRefinementConfig(config, "maxCopilotRounds");
428
- const requireRetrospectiveGate = resolveWorkflowConfig(config, "requireRetrospectiveGate");
429
- const requireRetrospectiveInternalTooling = resolveWorkflowConfig(config, "requireRetrospectiveInternalTooling");
430
- const retrospectiveCheckpoint = await loadRetrospectiveCheckpoint(repoRoot);
519
+ // Shared with interpretLoopState (consumed by copilot-pr-handoff.mjs) and
520
+ // evaluatePrGateCoordination the single source of truth for "is the
521
+ // Copilot round cap reached" so this detector cannot disagree with the
522
+ // handoff at the cap boundary (#1126).
523
+ const roundCapReached = isCopilotRoundCapReached({
524
+ copilotReviewRoundCount: context.snapshot?.copilotReviewRoundCount,
525
+ maxCopilotRounds,
526
+ });
527
+ const postConvergenceSignificantChange = await detectPostConvergenceSignificantChange(
528
+ {
529
+ repo: context.repo,
530
+ pr: context.pr,
531
+ currentHeadSha: context.currentHeadSha,
532
+ reviews: context.prData?.reviews,
533
+ changedFiles: context.prData?.files,
534
+ roundCapReached: roundCapReached && context.interpretation?.roundCapCleanEligible === true,
535
+ regularCopilotRounds: (context.snapshot?.copilotReviewRoundCount ?? 0) > 0,
536
+ },
537
+ runtime,
538
+ );
431
539
  const result = evaluatePrGateCoordination({
432
540
  repo: context.repo,
433
541
  pr: context.pr,
@@ -439,6 +547,9 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
439
547
  mergeStateStatus: context.mergeStateStatus,
440
548
  mergeable: context.mergeable,
441
549
  conflictFiles: context.conflictFiles,
550
+ // UI e2e auto-scoping (#976): path-triggered + fail-closed precondition.
551
+ changedFiles: extractChangedFiles(context.prData),
552
+ uiE2ePassed: deriveUiE2ePassed(context.prData),
442
553
  lifecycleState: context.interpretation.state,
443
554
  loopDisposition: context.disposition.loopDisposition,
444
555
  ciStatus: context.snapshot?.ciStatus ?? null,
@@ -446,14 +557,12 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
446
557
  maxCopilotRounds,
447
558
  sameHeadCleanConverged: context.interpretation.sameHeadCleanConverged,
448
559
  draftGateRequireCi: draftGateConfig.requireCi,
449
- requireRetrospectiveGate,
450
- requireRetrospectiveInternalTooling,
451
- retrospectiveCheckpoint,
452
560
  draftGate: context.gateEvidence.draftGate,
453
561
  draftGateMarker: context.gateEvidence.draftGateMarker,
454
562
  preApprovalGate: context.gateEvidence.preApprovalGate,
455
563
  preApprovalGateMarker: context.gateEvidence.preApprovalGateMarker,
456
564
  refinementArtifact: context.refinementArtifact,
565
+ postConvergenceSignificantChange,
457
566
  });
458
567
  // Copilot review request guard (#613): When Copilot has reviewed the PR
459
568
  // but no formal review request was made, block pre-approval gate entry.
@@ -465,9 +574,6 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
465
574
  PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
466
575
  PR_CHECKPOINT.FINAL_APPROVAL_READY,
467
576
  ]);
468
- const roundCapReached = maxCopilotRounds !== null
469
- && typeof (context.snapshot?.copilotReviewRoundCount) === "number"
470
- && context.snapshot?.copilotReviewRoundCount >= maxCopilotRounds;
471
577
  const sameHeadCleanConverged = context.interpretation?.sameHeadCleanConverged ?? false;
472
578
  // Round-cap clean fallback (#896): the interpreter resolved a clean post-cap head
473
579
  // (zero unresolved threads + green CI) that Copilot will not re-review. The formal
@@ -475,7 +581,13 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
475
581
  const roundCapCleanFallback = context.interpretation?.roundCapCleanEligible ?? false;
476
582
  const copilotReviewEverFormallyRequested = copilotReviewRequestStatus === "none"
477
583
  && guardBoundaries.has(result.gateBoundary)
478
- && !(roundCapReached && (sameHeadCleanConverged || roundCapCleanFallback))
584
+ // cap-0 disables the Copilot gate, so shouldGuardCopilotReviewRequest always
585
+ // returns false here — skip the timeline fetch it would never need (#1126).
586
+ // (Restores the suppression the roundCapReached predicate swap dropped.)
587
+ && maxCopilotRounds !== 0
588
+ && !(roundCapReached
589
+ && (sameHeadCleanConverged || roundCapCleanFallback)
590
+ && !postConvergenceSignificantChange)
479
591
  ? await fetchCopilotEverFormallyRequested(
480
592
  { repo: context.repo, pr: context.pr },
481
593
  runtime,
@@ -488,6 +600,7 @@ export async function detectPrGateCoordinationState(options, runtime = {}) {
488
600
  maxCopilotRounds,
489
601
  sameHeadCleanConverged,
490
602
  roundCapCleanFallback,
603
+ postConvergenceSignificantChange,
491
604
  gateBoundary: result.gateBoundary,
492
605
  })) {
493
606
  result.gateBoundary = PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW;
@@ -560,7 +673,7 @@ async function main() {
560
673
  }
561
674
  try {
562
675
  const result = await detectPrGateCoordinationState(options);
563
- process.stdout.write(`${JSON.stringify(result)}\n`);
676
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
564
677
  } catch (error) {
565
678
  process.stderr.write(`${formatCliError(error)}\n`);
566
679
  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)) {