dev-loops 0.6.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 (172) 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/{auto.md → loop-auto.md} +1 -1
  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/{info.md → loop-info.md} +2 -2
  9. package/.claude/commands/loop-queue-status.md +24 -0
  10. package/.claude/commands/{start-spike.md → loop-start-spike.md} +1 -1
  11. package/.claude/commands/{start.md → loop-start.md} +1 -1
  12. package/.claude/commands/{status.md → loop-status.md} +1 -1
  13. package/.claude/hooks/_bash-command-classify.mjs +333 -29
  14. package/.claude/hooks/_hook-decisions.mjs +138 -15
  15. package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
  16. package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
  17. package/.claude/skills/dev-loop/SKILL.md +15 -11
  18. package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
  19. package/.claude/skills/docs/anti-patterns.md +2 -1
  20. package/.claude/skills/docs/artifact-authority-contract.md +22 -4
  21. package/.claude/skills/docs/copilot-loop-operations.md +1 -1
  22. package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
  23. package/.claude/skills/docs/issue-intake-procedure.md +1 -0
  24. package/.claude/skills/docs/local-planning-flow.md +1 -1
  25. package/.claude/skills/docs/local-planning-worked-example.md +1 -1
  26. package/.claude/skills/docs/merge-preconditions.md +17 -1
  27. package/.claude/skills/docs/plan-file-contract.md +1 -1
  28. package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
  29. package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
  30. package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
  31. package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
  32. package/.claude/skills/local-implementation/SKILL.md +25 -14
  33. package/.claude/skills/loop-grill/SKILL.md +163 -0
  34. package/AGENTS.md +1 -0
  35. package/CHANGELOG.md +95 -0
  36. package/README.md +9 -9
  37. package/agents/dev-loop.agent.md +3 -2
  38. package/agents/developer.agent.md +1 -1
  39. package/agents/docs.agent.md +1 -1
  40. package/agents/fixer.agent.md +1 -1
  41. package/agents/quality.agent.md +1 -1
  42. package/agents/refiner.agent.md +1 -1
  43. package/agents/review.agent.md +3 -2
  44. package/cli/index.mjs +35 -42
  45. package/extension/README.md +4 -4
  46. package/extension/checks.ts +1 -5
  47. package/extension/harness-types.ts +1 -0
  48. package/extension/index.ts +6 -0
  49. package/extension/pi-extension-adapter.ts +2 -0
  50. package/extension/presentation.ts +7 -19
  51. package/package.json +9 -10
  52. package/scripts/github/capture-review-threads.mjs +2 -9
  53. package/scripts/github/comment-issue.mjs +2 -9
  54. package/scripts/github/create-label.mjs +133 -0
  55. package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
  56. package/scripts/github/detect-linked-issue-pr.mjs +22 -6
  57. package/scripts/github/edit-pr.mjs +259 -0
  58. package/scripts/github/fetch-ci-logs.mjs +2 -9
  59. package/scripts/github/list-issues.mjs +2 -9
  60. package/scripts/github/manage-sub-issues.mjs +46 -10
  61. package/scripts/github/offer-human-handoff.mjs +8 -5
  62. package/scripts/github/post-gate-findings.mjs +14 -2
  63. package/scripts/github/probe-ci-status.mjs +8 -2
  64. package/scripts/github/probe-copilot-review.mjs +21 -14
  65. package/scripts/github/ready-for-review.mjs +26 -8
  66. package/scripts/github/reconcile-draft-gate.mjs +7 -3
  67. package/scripts/github/reply-resolve-review-thread.mjs +16 -5
  68. package/scripts/github/reply-resolve-review-threads.mjs +69 -7
  69. package/scripts/github/request-copilot-review.mjs +8 -2
  70. package/scripts/github/resolve-handoff-candidates.mjs +7 -3
  71. package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
  72. package/scripts/github/stage-reviewer-draft.mjs +10 -2
  73. package/scripts/github/tick-verified-checkboxes.mjs +202 -0
  74. package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
  75. package/scripts/github/verify-fresh-review-context.mjs +150 -31
  76. package/scripts/github/view-pr.mjs +150 -0
  77. package/scripts/github/write-gate-context.mjs +248 -61
  78. package/scripts/github/write-gate-findings-log.mjs +75 -1
  79. package/scripts/lib/jq-output.mjs +18 -0
  80. package/scripts/loop/_post-convergence-change.mjs +211 -0
  81. package/scripts/loop/_pr-runner-coordination.mjs +70 -0
  82. package/scripts/loop/_repo-root-resolver.mjs +47 -0
  83. package/scripts/loop/build-handoff-envelope.mjs +14 -4
  84. package/scripts/loop/check-retro-tooling.mjs +14 -3
  85. package/scripts/loop/checkpoint-contract.mjs +7 -4
  86. package/scripts/loop/cleanup-worktree.mjs +12 -3
  87. package/scripts/loop/conductor-monitor.mjs +12 -18
  88. package/scripts/loop/copilot-pr-handoff.mjs +135 -14
  89. package/scripts/loop/debt-remediate.mjs +24 -12
  90. package/scripts/loop/detect-change-scope.mjs +36 -7
  91. package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
  92. package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
  93. package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
  94. package/scripts/loop/detect-internal-only-pr.mjs +8 -2
  95. package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
  96. package/scripts/loop/detect-pr-gate-coordination-state.mjs +149 -66
  97. package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
  98. package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
  99. package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
  100. package/scripts/loop/ensure-worktree.mjs +8 -3
  101. package/scripts/loop/info.mjs +12 -4
  102. package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
  103. package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
  104. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
  105. package/scripts/loop/inspect-run-viewer.mjs +67 -4
  106. package/scripts/loop/inspect-run.mjs +8 -2
  107. package/scripts/loop/outer-loop.mjs +8 -4
  108. package/scripts/loop/pr-runner-coordination.mjs +2 -9
  109. package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
  110. package/scripts/loop/pre-flight-gate.mjs +9 -9
  111. package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
  112. package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
  113. package/scripts/loop/provision-worktree.mjs +74 -3
  114. package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
  115. package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
  116. package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
  117. package/scripts/loop/run-conductor-cycle.mjs +8 -2
  118. package/scripts/loop/run-queue.mjs +18 -9
  119. package/scripts/loop/run-refinement-audit.mjs +8 -9
  120. package/scripts/loop/run-watch-cycle.mjs +2 -9
  121. package/scripts/loop/sanctioned-commands.mjs +104 -0
  122. package/scripts/loop/steer-loop.mjs +29 -16
  123. package/scripts/loop/validate-pr-body-spec.mjs +207 -0
  124. package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
  125. package/scripts/pages/build-site.mjs +59 -8
  126. package/scripts/pages/build-state-atlas.mjs +441 -0
  127. package/scripts/projects/_resolve-project.mjs +148 -0
  128. package/scripts/projects/add-queue-item.mjs +60 -54
  129. package/scripts/projects/archive-done-items.mjs +49 -84
  130. package/scripts/projects/ensure-queue-board.mjs +10 -36
  131. package/scripts/projects/list-queue-items.mjs +116 -65
  132. package/scripts/projects/move-queue-item.mjs +28 -49
  133. package/scripts/projects/reconcile-queue.mjs +253 -0
  134. package/scripts/projects/reorder-queue-item.mjs +41 -47
  135. package/scripts/projects/resolve-active-board-item.mjs +108 -46
  136. package/scripts/projects/sync-item-status.mjs +15 -10
  137. package/scripts/refine/_refine-helpers.mjs +21 -5
  138. package/scripts/refine/exit-spike.mjs +18 -8
  139. package/scripts/refine/promote-plan.mjs +22 -16
  140. package/scripts/refine/prose-linkage-detector.mjs +1 -1
  141. package/scripts/refine/refine-plan-file.mjs +13 -4
  142. package/scripts/refine/refinement-completeness-checker.mjs +1 -1
  143. package/scripts/refine/scaffold-spike-file.mjs +4 -8
  144. package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
  145. package/scripts/refine/tree-integrity-validator.mjs +1 -1
  146. package/scripts/refine/validate-plan-file.mjs +1 -1
  147. package/scripts/refine/validate-spike-file.mjs +1 -1
  148. package/scripts/refine/verify.mjs +11 -6
  149. package/scripts/release/assert-core-dependency-version.mjs +123 -0
  150. package/scripts/release/extract-changelog-section.mjs +16 -2
  151. package/skills/copilot-pr-followup/SKILL.md +24 -12
  152. package/skills/dev-loop/SKILL.md +10 -6
  153. package/skills/docs/acceptance-criteria-verification.md +6 -2
  154. package/skills/docs/anti-patterns.md +2 -1
  155. package/skills/docs/artifact-authority-contract.md +22 -4
  156. package/skills/docs/copilot-loop-operations.md +1 -1
  157. package/skills/docs/cross-harness-regression-contract.md +60 -0
  158. package/skills/docs/issue-intake-procedure.md +1 -0
  159. package/skills/docs/local-planning-flow.md +1 -1
  160. package/skills/docs/local-planning-worked-example.md +1 -1
  161. package/skills/docs/merge-preconditions.md +17 -1
  162. package/skills/docs/plan-file-contract.md +1 -1
  163. package/skills/docs/public-dev-loop-contract.md +1 -1
  164. package/skills/docs/retrospective-checkpoint-contract.md +90 -76
  165. package/skills/docs/ui-e2e-scoping-step.md +32 -0
  166. package/skills/docs/workflow-handoff-contract.md +39 -0
  167. package/skills/local-implementation/SKILL.md +25 -14
  168. package/skills/loop-grill/SKILL.md +165 -0
  169. package/.claude/commands/continue.md +0 -15
  170. package/scripts/loop/conductor.mjs +0 -233
  171. package/scripts/loop/detect-stale-runner.mjs +0 -265
  172. package/scripts/loop/pre-push-main-guard.mjs +0 -117
@@ -9,15 +9,19 @@ import {
9
9
  summarizeGateReviewCommentMarkers,
10
10
  summarizeGateReviewComments,
11
11
  } from "../_core-helpers.mjs";
12
- import { access } from "node:fs/promises";
12
+ import { access, readFile } from "node:fs/promises";
13
13
  import path from "node:path";
14
14
  import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
15
15
  import { fetchGithubReviewThreadsPayload } from "./capture-review-threads.mjs";
16
16
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
17
- import { loadDevLoopConfig, resolveGateConfig, resolveRequireFanoutEvidence } from "@dev-loops/core/config";
17
+ import { FANOUT_PROVENANCE_MIN_REVIEWERS, GATE_FULL_LABEL, loadDevLoopConfig, resolveGateConfig, resolveLightMode, resolveRequireFanoutEvidence, resolveRequireFanoutProvenance } from "@dev-loops/core/config";
18
+ import { FANOUT_UNAVAILABLE_MESSAGE, provenanceConsistencyError } from "@dev-loops/core/loop/gate-fanin";
19
+ import { detectMergeBaseScope, isEligibleForLightMode } from "../loop/detect-change-scope.mjs";
18
20
  import { buildLogPath } from "./write-gate-findings-log.mjs";
19
21
  import { ensureAsyncRunnerOwnership } from "../loop/_pr-runner-coordination.mjs";
20
22
  import { detectStaleRunner } from "../loop/_stale-runner-detection.mjs";
23
+ import { resolveLedgerCheckouts, resolveRepoRoot } from "../loop/_repo-root-resolver.mjs";
24
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
21
25
  const USAGE = `Usage: detect-checkpoint-evidence.mjs --repo <owner/name> --pr <number>
22
26
  Fetch the live PR head SHA and visible PR issue comments, then summarize the
23
27
  latest valid draft-gate and pre-approval checkpoint verdict comments. Always fail
@@ -84,9 +88,11 @@ Output (stdout, JSON; always includes preMergeGateCheck):
84
88
  Error output (stderr, JSON):
85
89
  { "ok": false, "error": "...", "usage": "..." }
86
90
  { "ok": false, "error": "..." }
91
+ ${JQ_OUTPUT_USAGE}
87
92
  Exit codes:
88
93
  0 Success (gate evidence is valid)
89
- 1 Argument error, gh failure, malformed gh JSON, or missing required pre-merge gate evidence.`.trim();
94
+ 1 Argument error, gh failure, malformed gh JSON, or missing required pre-merge gate evidence.
95
+ 2 Invalid --jq filter.`.trim();
90
96
  const parseError = buildParseError(USAGE);
91
97
  export function parseDetectCheckpointEvidenceCliArgs(argv) {
92
98
  const { tokens } = parseArgs({
@@ -96,6 +102,7 @@ export function parseDetectCheckpointEvidenceCliArgs(argv) {
96
102
  repo: { type: "string" },
97
103
  pr: { type: "string" },
98
104
  "require-before-merge": { type: "boolean" },
105
+ ...JQ_OUTPUT_PARSE_OPTIONS,
99
106
  },
100
107
  allowPositionals: true,
101
108
  strict: false,
@@ -125,6 +132,7 @@ export function parseDetectCheckpointEvidenceCliArgs(argv) {
125
132
  options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
126
133
  continue;
127
134
  }
135
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
128
136
  if (token.name === "require-before-merge") {
129
137
  throw parseError(`--require-before-merge has been removed: gate evidence enforcement is now always-on by default. Omit the flag.`);
130
138
  }
@@ -249,7 +257,24 @@ export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, s
249
257
  // { required: false, gates: [] } so the `.required` guard skips this block.
250
258
  if (fanoutEnforcement && fanoutEnforcement.required) {
251
259
  for (const gate of fanoutEnforcement.gates) {
252
- if (gate.executionMode !== "fanout_fanin") {
260
+ // Light-mode acceptance (#1174): a genuinely under-threshold micro-PR
261
+ // collapses the gate fan-out to a single inline check (#1043). Accept that
262
+ // inline verdict ONLY when ALL hold, fail CLOSED otherwise:
263
+ // - lightMode is enabled in config, AND
264
+ // - the reviewed head's merge-base scope was RE-DERIVED under threshold
265
+ // (scopeUnderThreshold; false whenever scope could not be derived), AND
266
+ // - the PR carries no gate:full label (which always forces fan-out), AND
267
+ // - the verdict records a non-empty inline reason.
268
+ // Any non-light inline verdict (over threshold / label / lightMode off /
269
+ // scope underivable) falls through to the byte-identical rejection below.
270
+ const lightAccepted =
271
+ gate.executionMode === "inline_single_agent"
272
+ && fanoutEnforcement.lightMode === true
273
+ && fanoutEnforcement.hasFullLabel !== true
274
+ && gate.scopeUnderThreshold === true
275
+ && typeof gate.inlineReason === "string"
276
+ && gate.inlineReason.trim().length > 0;
277
+ if (gate.executionMode !== "fanout_fanin" && !lightAccepted) {
253
278
  failures.push(
254
279
  `${gate.name}: requireFanoutEvidence is enabled but executionMode is "${gate.executionMode ?? "unset"}" (expected "fanout_fanin"); inline gate verdicts are not accepted`,
255
280
  );
@@ -259,6 +284,31 @@ export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, s
259
284
  failures.push(
260
285
  `${gate.name}: requireFanoutEvidence is enabled but no findings-log ledger exists for the reviewed head (${gate.ledgerPath})`,
261
286
  );
287
+ continue;
288
+ }
289
+ // Opt-in provenance enforcement (gates.requireFanoutProvenance), layered on
290
+ // top of fan-out evidence. When off (default) requireProvenance is falsy so
291
+ // NO new failure is added — behavior is byte-identical to today. When on, a
292
+ // fanout_fanin ledger must record INTERNALLY-CONSISTENT provenance (checked
293
+ // the same way the write path validates it — a hand-edited or shadow ledger
294
+ // is re-validated here, not trusted) with distinctReviewers >= the floor.
295
+ // Provenance is enforced ONLY for fanout_fanin verdicts (#1174): a
296
+ // light-accepted inline verdict is already scope-bounded and has no
297
+ // multi-reviewer provenance to record, so requiring it would make the
298
+ // light path unmergeable — the inverse of this issue's fix.
299
+ if (fanoutEnforcement.requireProvenance && gate.executionMode === "fanout_fanin") {
300
+ const prov = gate.provenance;
301
+ const consistencyErr = provenanceConsistencyError(prov);
302
+ const reviewers = prov && Number.isInteger(prov.distinctReviewers) ? prov.distinctReviewers : null;
303
+ if (consistencyErr) {
304
+ failures.push(
305
+ `${gate.name}: requireFanoutProvenance is enabled but the findings-log ledger lacks valid fan-out provenance (${consistencyErr}); ${FANOUT_UNAVAILABLE_MESSAGE}`,
306
+ );
307
+ } else if (reviewers === null || reviewers < FANOUT_PROVENANCE_MIN_REVIEWERS) {
308
+ failures.push(
309
+ `${gate.name}: requireFanoutProvenance is enabled but the findings-log ledger lacks valid fan-out provenance (need provenance.distinctReviewers >= ${FANOUT_PROVENANCE_MIN_REVIEWERS}, got ${reviewers === null ? "none" : reviewers}); ${FANOUT_UNAVAILABLE_MESSAGE}`,
310
+ );
311
+ }
262
312
  }
263
313
  }
264
314
  }
@@ -287,24 +337,88 @@ async function ledgerExists(fullPath) {
287
337
  return false;
288
338
  }
289
339
  }
340
+ /**
341
+ * True if the ledger (relative path) exists under ANY enumerated checkout
342
+ * (main + every worktree). Fixes #1050: a ledger written in the PR worktree is
343
+ * found even when the check runs from a different checkout's git-toplevel.
344
+ */
345
+ async function ledgerExistsInAny(checkouts, ledgerPath) {
346
+ for (const root of checkouts) {
347
+ if (await ledgerExists(path.resolve(root, ledgerPath))) {
348
+ return true;
349
+ }
350
+ }
351
+ return false;
352
+ }
353
+ /**
354
+ * Read the recorded fan-out `provenance` object from a ledger across the
355
+ * enumerated checkouts. Mirrors ledgerExistsInAny's "ANY checkout satisfies"
356
+ * semantics: prefers the FIRST checkout whose ledger provenance actually
357
+ * SATISFIES enforcement (internally consistent AND distinctReviewers >= floor),
358
+ * so a below-floor or provenance-less ledger in an earlier-enumerated checkout
359
+ * cannot SHADOW a valid one in the PR worktree (which would falsely fail closed).
360
+ * Falls back to the first non-null provenance (for a useful diagnostic message)
361
+ * only when NO checkout satisfies, and null only when none is present. Only
362
+ * called when requireFanoutProvenance is enabled so the default path pays no I/O.
363
+ */
364
+ async function readLedgerProvenanceInAny(checkouts, ledgerPath) {
365
+ let firstNonNull = null;
366
+ for (const root of checkouts) {
367
+ const full = path.resolve(root, ledgerPath);
368
+ try {
369
+ const parsed = JSON.parse(await readFile(full, "utf8"));
370
+ const prov = parsed && typeof parsed === "object" ? parsed.provenance : null;
371
+ if (prov == null) continue; // ledger present but no provenance — keep scanning.
372
+ if (provenanceConsistencyError(prov) === null && prov.distinctReviewers >= FANOUT_PROVENANCE_MIN_REVIEWERS) {
373
+ return prov; // satisfying ledger — prefer it over any earlier below-floor one.
374
+ }
375
+ if (firstNonNull === null) firstNonNull = prov; // remember for diagnostics.
376
+ } catch {
377
+ // Missing/unreadable/malformed ledger in this checkout — try the next.
378
+ }
379
+ }
380
+ return firstNonNull;
381
+ }
290
382
  /**
291
383
  * Build the fan-out evidence enforcement descriptor.
292
384
  *
293
385
  * Enforcement is ON by default (opt-out via gates.requireFanoutEvidence: false).
294
386
  * Returns { required: false } when enforcement is disabled OR when config is
295
387
  * unavailable (config == null — null or undefined — after a failed load) — config-unavailable must
296
- * fail open and never enable enforcement. When enabled, records per-required-gate
297
- * executionMode and whether the deterministic findings-log ledger exists for the
298
- * reviewed head SHA so the pre-merge check can fail closed on inline verdicts or
299
- * missing ledgers.
388
+ * fail open and never enable enforcement. When enabled, returns
389
+ * { required: true, requireProvenance, lightMode, hasFullLabel, gates } where
390
+ * each per-required-gate entry records executionMode, inlineReason,
391
+ * scopeUnderThreshold, and whether the deterministic findings-log ledger exists
392
+ * for the reviewed head SHA, so the pre-merge check can fail closed on inline
393
+ * verdicts or missing ledgers.
394
+ *
395
+ * Light mode (#1174): when gates.lightMode is configured, `hasFullLabel`
396
+ * (gate:full PR label) and `baseRef` feed a fail-closed merge-base scope
397
+ * re-derivation for inline verdicts. A gate's scopeUnderThreshold is true only
398
+ * when light mode is on, the PR has no gate:full label, a base ref is known,
399
+ * and the reviewed head's merge-base diff is genuinely under threshold — which
400
+ * lets the pre-merge check accept an inline single-agent verdict for a
401
+ * small-scope PR instead of always rejecting inline evidence.
300
402
  */
301
- async function buildFanoutEnforcement({ repo, pr, currentHeadSha, draftGateMarker, preApprovalGateMarker, config, cwd }) {
403
+ export async function buildFanoutEnforcement({ repo, pr, currentHeadSha, draftGateMarker, preApprovalGateMarker, config, cwd, hasFullLabel = false, baseRef = null }) {
302
404
  // Fail open when config could not be loaded/validated. `== null` covers both
303
405
  // null and undefined; the loader only ever yields null on failure, but the
304
406
  // loose check defensively treats an absent config as unavailable.
305
407
  if (config == null || !resolveRequireFanoutEvidence(config)) {
408
+ // Disabled/unavailable return is intentionally byte-identical to before
409
+ // (no requireProvenance key): buildPreMergeGateCheck only reads it inside
410
+ // the `required` block, so this preserves the exact existing shape.
306
411
  return { required: false, gates: [] };
307
412
  }
413
+ // Provenance enforcement is opt-in and layered ON TOP of fan-out evidence: it
414
+ // only takes effect while evidence enforcement (above) is active.
415
+ const requireProvenance = resolveRequireFanoutProvenance(config);
416
+ // Light-mode facts (#1174): the threshold that a re-derived merge-base scope
417
+ // must fall under for an inline verdict to be accepted. null when lightMode is
418
+ // disabled → no inline verdict can ever be accepted (scopeUnderThreshold stays
419
+ // false), preserving today's rejection.
420
+ const lightThreshold = resolveLightMode(config);
421
+ const lightMode = lightThreshold != null;
308
422
  const draftRequired = resolveGateConfig(config, "draft").required;
309
423
  const preApprovalRequired = resolveGateConfig(config, "preApproval").required;
310
424
  const gateSpecs = [
@@ -312,18 +426,35 @@ async function buildFanoutEnforcement({ repo, pr, currentHeadSha, draftGateMarke
312
426
  { name: "pre_approval_gate", marker: preApprovalGateMarker, required: preApprovalRequired },
313
427
  ].filter((spec) => spec.required && spec.marker.visible);
314
428
  const gates = [];
429
+ const checkouts = resolveLedgerCheckouts(cwd);
430
+ // checkouts[0] is always resolveRepoRoot(cwd) (resolveLedgerCheckouts adds it
431
+ // first, unconditionally, and never throws — it falls back to cwd on git
432
+ // failure) — reuse it instead of a second `git rev-parse --show-toplevel`.
433
+ const repoRoot = checkouts[0];
315
434
  for (const spec of gateSpecs) {
316
435
  const headSha = spec.marker.headSha ?? currentHeadSha;
317
436
  const ledgerPath = buildLogPath({ repo, pr, gate: spec.name, headSha, tmpRoot: "tmp" });
318
- const fullPath = path.resolve(cwd, ledgerPath);
437
+ // Re-derive scope FAIL-CLOSED for inline verdicts only (the fan-out default
438
+ // path pays no git I/O). scopeUnderThreshold is true ONLY when lightMode is
439
+ // on, the PR has no gate:full label, a base ref is known, and the merge-base
440
+ // diff for the reviewed head is genuinely under threshold. Any git/scope
441
+ // failure leaves it false, so the inline verdict is rejected exactly as today.
442
+ let scopeUnderThreshold = false;
443
+ if (lightMode && !hasFullLabel && baseRef && spec.marker.executionMode === "inline_single_agent") {
444
+ const scope = detectMergeBaseScope({ base: baseRef, head: headSha, cwd: repoRoot });
445
+ scopeUnderThreshold = scope.ok === true && isEligibleForLightMode(scope, lightThreshold);
446
+ }
319
447
  gates.push({
320
448
  name: spec.name,
321
449
  executionMode: spec.marker.executionMode ?? null,
450
+ inlineReason: spec.marker.inlineReason ?? null,
451
+ scopeUnderThreshold,
322
452
  ledgerPath,
323
- ledgerExists: await ledgerExists(fullPath),
453
+ ledgerExists: await ledgerExistsInAny(checkouts, ledgerPath),
454
+ provenance: requireProvenance ? await readLedgerProvenanceInAny(checkouts, ledgerPath) : null,
324
455
  });
325
456
  }
326
- return { required: true, gates };
457
+ return { required: true, requireProvenance, lightMode, hasFullLabel, gates };
327
458
  }
328
459
  export async function detectCheckpointEvidence(options, { env = process.env, ghCommand = "gh", cwd = process.cwd() } = {}) {
329
460
  const runnerOwnership = await ensureAsyncRunnerOwnership({
@@ -378,8 +509,31 @@ export async function detectCheckpointEvidence(options, { env = process.env, ghC
378
509
  // treat it as config-unavailable and leave fan-out enforcement disabled
379
510
  // (preserves default behavior). Other gate checks remain unaffected.
380
511
  let config = null;
381
- const { config: loadedConfig, errors: configErrors } = await loadDevLoopConfig({ repoRoot: cwd });
512
+ const { config: loadedConfig, errors: configErrors } = await loadDevLoopConfig({ repoRoot: resolveRepoRoot(cwd) });
382
513
  config = Array.isArray(configErrors) && configErrors.length > 0 ? null : loadedConfig;
514
+ // Light-mode pre-merge facts (#1174): the base commit for the merge-base scope
515
+ // re-derivation and whether the PR forces full fan-out via the gate:full label.
516
+ // Fetched LAZILY — only when fan-out enforcement is active AND lightMode is on
517
+ // AND a gate actually recorded an inline verdict — so the common fan-out path
518
+ // (and every existing caller/test) makes NO extra gh call and stays unchanged.
519
+ let baseRef = null;
520
+ let hasFullLabel = false;
521
+ const anyInlineVerdict = [draftGateMarker, preApprovalGateMarker].some(
522
+ (marker) => marker.visible && marker.executionMode === "inline_single_agent",
523
+ );
524
+ if (config != null && resolveRequireFanoutEvidence(config) && resolveLightMode(config) != null && anyInlineVerdict) {
525
+ try {
526
+ const lightFacts = await runGhJson(["pr", "view", String(options.pr), "--repo", options.repo, "--json", "baseRefOid,labels"], { env, ghCommand });
527
+ baseRef = typeof lightFacts?.baseRefOid === "string" && lightFacts.baseRefOid.trim().length > 0
528
+ ? lightFacts.baseRefOid.trim()
529
+ : null;
530
+ hasFullLabel = Array.isArray(lightFacts?.labels)
531
+ && lightFacts.labels.some((label) => (typeof label === "string" ? label : label?.name) === GATE_FULL_LABEL);
532
+ } catch {
533
+ // Fail CLOSED: without the label/base facts we cannot safely accept an
534
+ // inline verdict, so leave baseRef null (scope underivable → rejected).
535
+ }
536
+ }
383
537
  const fanoutEnforcement = await buildFanoutEnforcement({
384
538
  repo: options.repo,
385
539
  pr: options.pr,
@@ -388,6 +542,8 @@ export async function detectCheckpointEvidence(options, { env = process.env, ghC
388
542
  preApprovalGateMarker,
389
543
  config,
390
544
  cwd,
545
+ hasFullLabel,
546
+ baseRef,
391
547
  });
392
548
  return {
393
549
  ok: true,
@@ -458,7 +614,7 @@ async function main() {
458
614
  process.exitCode = 1;
459
615
  return;
460
616
  }
461
- process.stdout.write(`${JSON.stringify(output)}\n`);
617
+ process.exitCode = emitResult(output, { jq: options.jq, silent: options.silent });
462
618
  } catch (error) {
463
619
  if (error && typeof error === "object" && "staleRunner" in error && error.staleRunner) {
464
620
  const staleRunnerCheck = {
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from "node:util";
3
3
  import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
4
- import { parseIssueNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
4
+ import { parseIssueNumber, requireTokenValue, runChild as defaultRunChild } 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
  export const LINKED_ISSUE_PR_QUERY = [
7
8
  "query($owner:String!, $name:String!, $issue:Int!, $after:String) {",
8
9
  " repository(owner:$owner, name:$name) {",
@@ -28,6 +29,7 @@ export const LINKED_ISSUE_PR_QUERY = [
28
29
  " }",
29
30
  " ... on CrossReferencedEvent {",
30
31
  " createdAt",
32
+ " willCloseTarget",
31
33
  " source {",
32
34
  " __typename",
33
35
  " ... on PullRequest {",
@@ -73,12 +75,18 @@ Error output (stderr, JSON):
73
75
  Argument/usage errors:
74
76
  { "ok": false, "error": "...", "usage": "..." }
75
77
  gh/runtime failures:
76
- { "ok": false, "error": "..." }`.trim();
78
+ { "ok": false, "error": "..." }
79
+ ${JQ_OUTPUT_USAGE}`.trim();
77
80
  const parseError = buildParseError(USAGE);
78
81
  export function parseDetectLinkedIssuePrCliArgs(argv) {
79
82
  const { tokens } = parseArgs({
80
83
  args: [...argv],
81
- options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, issue: { type: "string" } },
84
+ options: {
85
+ help: { type: "boolean", short: "h" },
86
+ repo: { type: "string" },
87
+ issue: { type: "string" },
88
+ ...JQ_OUTPUT_PARSE_OPTIONS,
89
+ },
82
90
  allowPositionals: true,
83
91
  strict: false,
84
92
  tokens: true,
@@ -107,6 +115,7 @@ export function parseDetectLinkedIssuePrCliArgs(argv) {
107
115
  options.issue = parseIssueNumber(requireTokenValue(token, parseError), parseError);
108
116
  continue;
109
117
  }
118
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
110
119
  throw parseError(`Unknown argument: ${token.rawName}`);
111
120
  }
112
121
  if (options.repo === undefined || options.issue === undefined) {
@@ -162,6 +171,13 @@ function normalizeLinkedPrNode(node) {
162
171
  };
163
172
  }
164
173
  if (node.__typename === "CrossReferencedEvent") {
174
+ // Only a cross-reference that will CLOSE this issue owns its board status.
175
+ // A bare body-mention (willCloseTarget:false, e.g. "part of #X") must not
176
+ // create board-ownership linkage — otherwise every sibling a PR mentions
177
+ // gets dragged to In Progress and the resolver fails closed (#1130).
178
+ if (node.willCloseTarget !== true) {
179
+ return null;
180
+ }
165
181
  return {
166
182
  eventType: "CROSS_REFERENCED_EVENT",
167
183
  eventCreatedAt: node.createdAt,
@@ -253,7 +269,7 @@ export function selectLinkedIssuePr(candidates) {
253
269
  });
254
270
  return sorted[0] ?? null;
255
271
  }
256
- export async function detectLinkedIssuePr({ repo, issue }, { env = process.env, ghCommand = "gh" } = {}) {
272
+ export async function detectLinkedIssuePr({ repo, issue }, { env = process.env, ghCommand = "gh", runChild = defaultRunChild } = {}) {
257
273
  const { owner, name } = parseRepoSlug(repo);
258
274
  const candidates = [];
259
275
  const closedUnmergedCandidates = [];
@@ -322,7 +338,7 @@ export async function detectLinkedIssuePr({ repo, issue }, { env = process.env,
322
338
  }
323
339
  export async function runCli(
324
340
  argv = process.argv.slice(2),
325
- { stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
341
+ { stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
326
342
  ) {
327
343
  const options = parseDetectLinkedIssuePrCliArgs(argv);
328
344
  if (options.help) {
@@ -333,7 +349,7 @@ export async function runCli(
333
349
  { repo: options.repo, issue: options.issue },
334
350
  { env, ghCommand },
335
351
  );
336
- stdout.write(`${JSON.stringify(result)}\n`);
352
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
337
353
  }
338
354
  if (isDirectCliRun(import.meta.url)) {
339
355
  runCli().catch((error) => {
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { readFileSync } from "node:fs";
4
+ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
5
+ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
6
+ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
7
+ import { parseArgs } from "node:util";
8
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
9
+
10
+ const USAGE = `Usage: edit-pr.mjs --repo <owner/name> --pr <number> [--title <t>] [--body <b> | --body-file <path>] [--add-assignee <u>] [--remove-assignee <u>] [--milestone <m>]
11
+ Edit PR title/body/assignees/milestone. Thin wrapper over \`gh pr edit\` — use this
12
+ instead of an agent-level raw \`gh pr edit\` so the loop's internal-tooling record
13
+ stays clean (siblings: view-pr.mjs, comment-issue.mjs; #1057).
14
+ Required:
15
+ --repo <owner/name> Repository slug (e.g. owner/repo)
16
+ --pr <number> Pull request number
17
+ At least one edit:
18
+ --title <t> New PR title
19
+ --body <b> New PR body as a single argument
20
+ --body-file <path> Read the new body from a file (- reads stdin)
21
+ --add-assignee <u> Assignee to add (repeatable)
22
+ --remove-assignee <u> Assignee to remove (repeatable)
23
+ --milestone <m> Milestone to set (empty string clears it)
24
+ (--title/--body/--body-file reject empty or
25
+ whitespace-only values; use --milestone "" only
26
+ to clear the milestone)
27
+ Output (stdout, JSON):
28
+ { "ok": true, "repo": "owner/repo", "pr": 17, "edited": ["title", "body", ...] }
29
+ Error output (stderr, JSON):
30
+ { "ok": false, "error": "...", "usage"?: "..." }
31
+ ${JQ_OUTPUT_USAGE}
32
+ Exit codes:
33
+ 0 Success
34
+ 1 Argument error or gh failure
35
+ 2 Invalid --jq filter`.trim();
36
+ const parseError = buildParseError(USAGE);
37
+
38
+ export function parseEditPrCliArgs(argv) {
39
+ const { tokens } = parseArgs({
40
+ args: [...argv],
41
+ options: {
42
+ help: { type: "boolean", short: "h" },
43
+ repo: { type: "string" },
44
+ pr: { type: "string" },
45
+ title: { type: "string" },
46
+ body: { type: "string" },
47
+ "body-file": { type: "string" },
48
+ "add-assignee": { type: "string", multiple: true },
49
+ "remove-assignee": { type: "string", multiple: true },
50
+ milestone: { type: "string" },
51
+ ...JQ_OUTPUT_PARSE_OPTIONS,
52
+ },
53
+ allowPositionals: true,
54
+ strict: false,
55
+ tokens: true,
56
+ });
57
+ const options = {
58
+ help: false,
59
+ repo: undefined,
60
+ pr: undefined,
61
+ title: undefined,
62
+ body: undefined,
63
+ bodyFile: undefined,
64
+ addAssignees: [],
65
+ removeAssignees: [],
66
+ milestone: undefined,
67
+ jq: undefined,
68
+ silent: false,
69
+ };
70
+ for (const token of tokens) {
71
+ if (token.kind === "positional") {
72
+ throw parseError(`Unknown argument: ${token.value}`);
73
+ }
74
+ if (token.kind !== "option") {
75
+ continue;
76
+ }
77
+ if (token.name === "help") {
78
+ options.help = true;
79
+ return options;
80
+ }
81
+ if (token.name === "repo") {
82
+ options.repo = requireTokenValue(token, parseError).trim();
83
+ continue;
84
+ }
85
+ if (token.name === "pr") {
86
+ options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
87
+ continue;
88
+ }
89
+ if (token.name === "title") {
90
+ const title = requireTokenValue(token, parseError);
91
+ if (title.trim().length === 0) {
92
+ throw parseError("--title must not be empty or whitespace-only");
93
+ }
94
+ options.title = title;
95
+ continue;
96
+ }
97
+ if (token.name === "body") {
98
+ const body = requireTokenValue(token, parseError);
99
+ if (body.trim().length === 0) {
100
+ throw parseError("--body must not be empty or whitespace-only");
101
+ }
102
+ options.body = body;
103
+ continue;
104
+ }
105
+ if (token.name === "body-file") {
106
+ const rawPath = requireTokenValue(token, parseError).trim();
107
+ if (rawPath.length === 0) {
108
+ throw parseError("--body-file must be a non-empty path");
109
+ }
110
+ options.bodyFile = rawPath;
111
+ continue;
112
+ }
113
+ if (token.name === "add-assignee") {
114
+ const u = requireTokenValue(token, parseError).trim();
115
+ if (u.length === 0) throw parseError("--add-assignee must be a non-empty login");
116
+ options.addAssignees.push(u);
117
+ continue;
118
+ }
119
+ if (token.name === "remove-assignee") {
120
+ const u = requireTokenValue(token, parseError).trim();
121
+ if (u.length === 0) throw parseError("--remove-assignee must be a non-empty login");
122
+ options.removeAssignees.push(u);
123
+ continue;
124
+ }
125
+ if (token.name === "milestone") {
126
+ // Read the raw token value: an empty string is a valid milestone value
127
+ // (`gh pr edit --milestone ""` clears it), so this deliberately does NOT
128
+ // go through requireTokenValue (which rejects empty). Guard only a truly
129
+ // missing value (`--milestone` with no following token). A whitespace-only
130
+ // value is neither a clear nor a real milestone name — fail closed rather
131
+ // than forwarding it to gh for a less actionable error.
132
+ if (typeof token.value !== "string") {
133
+ throw parseError("--milestone requires a value (use an empty string to clear)");
134
+ }
135
+ if (token.value.length > 0 && token.value.trim().length === 0) {
136
+ throw parseError('--milestone must be a milestone name or "" to clear (whitespace-only is not allowed)');
137
+ }
138
+ options.milestone = token.value;
139
+ continue;
140
+ }
141
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
142
+ throw parseError(`Unknown argument: ${token.rawName}`);
143
+ }
144
+ if (options.repo === undefined || options.pr === undefined) {
145
+ throw parseError("Editing a PR requires both --repo <owner/name> and --pr <number>");
146
+ }
147
+ if (options.body !== undefined && options.bodyFile !== undefined) {
148
+ throw parseError("--body and --body-file are mutually exclusive; pass only one");
149
+ }
150
+ const hasEdit =
151
+ options.title !== undefined ||
152
+ options.body !== undefined ||
153
+ options.bodyFile !== undefined ||
154
+ options.addAssignees.length > 0 ||
155
+ options.removeAssignees.length > 0 ||
156
+ options.milestone !== undefined;
157
+ if (!hasEdit) {
158
+ throw parseError("Editing a PR requires at least one of --title/--body/--body-file/--add-assignee/--remove-assignee/--milestone");
159
+ }
160
+ try {
161
+ parseRepoSlug(options.repo);
162
+ } catch (error) {
163
+ throw parseError(error instanceof Error ? error.message : String(error));
164
+ }
165
+ return options;
166
+ }
167
+
168
+ async function resolveBody(options) {
169
+ if (options.bodyFile === undefined) return options.body;
170
+ // Stdin (fd 0): the fs/promises readFile does NOT accept an integer fd, so read
171
+ // it synchronously via the callback-style API (which does). A real path stays on
172
+ // the async promise read.
173
+ const body =
174
+ options.bodyFile === "-" ? readFileSync(0, "utf8") : await readFile(options.bodyFile, "utf8");
175
+ // Fail closed on an empty / whitespace-only file so a blank --body-file cannot
176
+ // silently clear the PR body (USAGE promises --body/--title reject empties).
177
+ if (body.trim().length === 0) {
178
+ throw new Error(`--body-file ${options.bodyFile} is empty`);
179
+ }
180
+ return body;
181
+ }
182
+
183
+ // Build the `gh pr edit` args and the parallel `edited` list (which fields were
184
+ // touched) so callers get a stable summary without re-reading the PR.
185
+ async function buildEditArgs(options) {
186
+ const args = ["pr", "edit", String(options.pr), "--repo", options.repo];
187
+ const edited = [];
188
+ if (options.title !== undefined) {
189
+ args.push("--title", options.title);
190
+ edited.push("title");
191
+ }
192
+ // resolveBody still runs for validation (reads the file, throws on empty /
193
+ // whitespace-only). A REAL --body-file path is handed straight to gh so large
194
+ // bodies avoid command-length limits. But `--body-file -` (stdin) was already
195
+ // consumed by resolveBody reading fd 0; re-emitting `--body-file -` makes gh
196
+ // re-read an exhausted stdin and clear the body, so pass the resolved string
197
+ // inline via --body instead.
198
+ const body = await resolveBody(options);
199
+ if (body !== undefined) {
200
+ if (options.bodyFile !== undefined && options.bodyFile !== "-") {
201
+ args.push("--body-file", options.bodyFile);
202
+ } else {
203
+ args.push("--body", body);
204
+ }
205
+ edited.push("body");
206
+ }
207
+ for (const u of options.addAssignees) {
208
+ args.push("--add-assignee", u);
209
+ }
210
+ if (options.addAssignees.length > 0) edited.push("add-assignee");
211
+ for (const u of options.removeAssignees) {
212
+ args.push("--remove-assignee", u);
213
+ }
214
+ if (options.removeAssignees.length > 0) edited.push("remove-assignee");
215
+ if (options.milestone !== undefined) {
216
+ args.push("--milestone", options.milestone);
217
+ edited.push("milestone");
218
+ }
219
+ return { args, edited };
220
+ }
221
+
222
+ export async function editPr(options, { env = process.env, ghCommand = "gh", run = runChild } = {}) {
223
+ const { args, edited } = await buildEditArgs(options);
224
+ const result = await run(ghCommand, args, env);
225
+ if (result.code !== 0) {
226
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
227
+ throw new Error(`gh pr edit failed: ${detail}`);
228
+ }
229
+ return { ok: true, repo: options.repo, pr: options.pr, edited };
230
+ }
231
+
232
+ export async function runCli(
233
+ argv = process.argv.slice(2),
234
+ { stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh", run = runChild } = {},
235
+ ) {
236
+ let options;
237
+ try {
238
+ options = parseEditPrCliArgs(argv);
239
+ } catch (error) {
240
+ stderr.write(`${formatCliError(error)}\n`);
241
+ return 1;
242
+ }
243
+ if (options.help) {
244
+ stdout.write(`${USAGE}\n`);
245
+ return 0;
246
+ }
247
+ let result;
248
+ try {
249
+ result = await editPr(options, { env, ghCommand, run });
250
+ } catch (error) {
251
+ stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
252
+ return 1;
253
+ }
254
+ return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
255
+ }
256
+
257
+ if (isDirectCliRun(import.meta.url)) {
258
+ runCli().then((code) => { process.exitCode = code; });
259
+ }
@@ -3,7 +3,7 @@ import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "
3
3
  import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
4
4
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
5
5
  import { parseArgs } from "node:util";
6
- import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
6
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
7
7
 
8
8
  const DEFAULT_TAIL_LINES = 200;
9
9
 
@@ -91,14 +91,7 @@ export function parseFetchCiLogsCliArgs(argv) {
91
91
  options.tail = value;
92
92
  continue;
93
93
  }
94
- if (token.name === "jq") {
95
- options.jq = requireTokenValue(token, parseError);
96
- continue;
97
- }
98
- if (token.name === "silent") {
99
- options.silent = true;
100
- continue;
101
- }
94
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
102
95
  throw parseError(`Unknown argument: ${token.rawName}`);
103
96
  }
104
97
  if (options.repo === undefined || options.pr === undefined) {