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.
- package/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +8 -7
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/refiner.md +2 -2
- package/.claude/agents/review.md +4 -3
- package/.claude/commands/{auto.md → loop-auto.md} +1 -1
- package/.claude/commands/loop-continue.md +15 -0
- package/.claude/commands/loop-enqueue.md +22 -0
- package/.claude/commands/loop-grill.md +17 -0
- package/.claude/commands/{info.md → loop-info.md} +2 -2
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/{start-spike.md → loop-start-spike.md} +2 -2
- package/.claude/commands/{start.md → loop-start.md} +1 -1
- package/.claude/commands/{status.md → loop-status.md} +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
- package/.claude/skills/copilot-pr-followup/SKILL.md +60 -34
- package/.claude/skills/dev-loop/SKILL.md +16 -12
- package/.claude/skills/docs/acceptance-criteria-verification.md +8 -3
- package/.claude/skills/docs/anti-patterns.md +5 -3
- package/.claude/skills/docs/artifact-authority-contract.md +30 -11
- package/.claude/skills/docs/confirmation-rules.md +1 -0
- package/.claude/skills/docs/contract-style-guide.md +37 -0
- package/.claude/skills/docs/copilot-loop-operations.md +19 -15
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/debt-remediation-contract.md +1 -1
- package/.claude/skills/docs/epic-tree-refinement-procedure.md +25 -22
- package/.claude/skills/docs/issue-intake-procedure.md +26 -19
- package/.claude/skills/docs/local-planning-flow.md +1 -1
- package/.claude/skills/docs/local-planning-worked-example.md +1 -1
- package/.claude/skills/docs/merge-preconditions.md +30 -12
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/pr-lifecycle-contract.md +44 -35
- package/.claude/skills/docs/public-dev-loop-contract.md +60 -44
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +106 -79
- package/.claude/skills/docs/spike-mode-contract.md +14 -7
- package/.claude/skills/docs/stop-conditions.md +30 -15
- package/.claude/skills/docs/tracker-first-loop-state.md +13 -8
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/validation-policy.md +4 -5
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +62 -145
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +101 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +11 -10
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +2 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +3 -3
- package/agents/review.agent.md +5 -4
- package/cli/index.mjs +35 -42
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +6 -0
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +7 -19
- package/package.json +12 -12
- package/scripts/_core-helpers.mjs +2 -0
- package/scripts/claude/headless-dev-loop.mjs +1 -1
- package/scripts/docs/validate-rule-ownership.mjs +442 -0
- package/scripts/docs/validate-state-machine-conformance.mjs +1092 -0
- package/scripts/github/_review-thread-mutations.mjs +5 -2
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +2 -9
- package/scripts/github/create-label.mjs +133 -0
- package/scripts/github/create-pr.mjs +119 -10
- package/scripts/github/detect-checkpoint-evidence.mjs +259 -18
- package/scripts/github/detect-linked-issue-pr.mjs +22 -6
- package/scripts/github/edit-pr.mjs +259 -0
- package/scripts/github/fetch-ci-logs.mjs +2 -9
- package/scripts/github/list-issues.mjs +2 -9
- package/scripts/github/list-review-threads.mjs +277 -0
- package/scripts/github/manage-sub-issues.mjs +46 -10
- package/scripts/github/offer-human-handoff.mjs +8 -5
- package/scripts/github/post-gate-findings.mjs +20 -5
- package/scripts/github/probe-ci-status.mjs +8 -2
- package/scripts/github/probe-copilot-review.mjs +21 -14
- package/scripts/github/ready-for-review.mjs +26 -8
- package/scripts/github/reconcile-draft-gate.mjs +7 -3
- package/scripts/github/reply-resolve-review-thread.mjs +16 -5
- package/scripts/github/reply-resolve-review-threads.mjs +69 -7
- package/scripts/github/request-copilot-review.mjs +98 -26
- package/scripts/github/resolve-handoff-candidates.mjs +7 -3
- package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
- package/scripts/github/stage-reviewer-draft.mjs +10 -2
- package/scripts/github/tick-verified-checkboxes.mjs +202 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +105 -15
- package/scripts/github/verify-briefing-prefixes.mjs +191 -0
- package/scripts/github/verify-fresh-review-context.mjs +226 -32
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/wait-pr-checks.mjs +171 -0
- package/scripts/github/write-gate-context.mjs +458 -65
- package/scripts/github/write-gate-findings-log.mjs +128 -2
- package/scripts/lib/jq-output.mjs +18 -0
- package/scripts/loop/_post-convergence-change.mjs +211 -0
- package/scripts/loop/_pr-runner-coordination.mjs +70 -0
- package/scripts/loop/_repo-root-resolver.mjs +47 -0
- package/scripts/loop/build-handoff-envelope.mjs +14 -4
- package/scripts/loop/check-retro-tooling.mjs +14 -3
- package/scripts/loop/checkpoint-contract.mjs +7 -4
- package/scripts/loop/cleanup-worktree.mjs +12 -3
- package/scripts/loop/conductor-monitor.mjs +12 -18
- package/scripts/loop/copilot-pr-handoff.mjs +162 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +38 -9
- package/scripts/loop/detect-copilot-loop-state.mjs +34 -8
- package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
- package/scripts/loop/detect-internal-only-pr.mjs +8 -2
- package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +184 -69
- package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
- package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
- package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
- package/scripts/loop/ensure-worktree.mjs +8 -3
- package/scripts/loop/info.mjs +12 -4
- package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
- package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
- package/scripts/loop/inspect-run-viewer.mjs +67 -4
- package/scripts/loop/inspect-run.mjs +8 -2
- package/scripts/loop/outer-loop.mjs +8 -4
- package/scripts/loop/pr-runner-coordination.mjs +2 -9
- package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
- package/scripts/loop/pre-flight-gate.mjs +9 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
- package/scripts/loop/provision-worktree.mjs +74 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +216 -10
- package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
- package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
- package/scripts/loop/run-conductor-cycle.mjs +8 -2
- package/scripts/loop/run-queue.mjs +18 -9
- package/scripts/loop/run-refinement-audit.mjs +8 -9
- package/scripts/loop/run-watch-cycle.mjs +2 -9
- package/scripts/loop/sanctioned-commands.mjs +106 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +223 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
- package/scripts/pages/build-site.mjs +59 -8
- package/scripts/pages/build-state-atlas.mjs +443 -0
- package/scripts/projects/_resolve-project.mjs +1 -0
- package/scripts/projects/add-queue-item.mjs +60 -54
- package/scripts/projects/archive-done-items.mjs +49 -84
- package/scripts/projects/ensure-queue-board.mjs +10 -36
- package/scripts/projects/list-queue-items.mjs +59 -382
- package/scripts/projects/move-queue-item.mjs +21 -449
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +43 -68
- package/scripts/projects/resolve-active-board-item.mjs +108 -46
- package/scripts/projects/sync-item-status.mjs +15 -10
- package/scripts/refine/_refine-helpers.mjs +21 -5
- package/scripts/refine/exit-spike.mjs +18 -8
- package/scripts/refine/promote-plan.mjs +22 -16
- package/scripts/refine/prose-linkage-detector.mjs +1 -1
- package/scripts/refine/refine-plan-file.mjs +13 -4
- package/scripts/refine/refinement-completeness-checker.mjs +1 -1
- package/scripts/refine/scaffold-spike-file.mjs +4 -8
- package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
- package/scripts/refine/tree-integrity-validator.mjs +1 -1
- package/scripts/refine/validate-plan-file.mjs +1 -1
- package/scripts/refine/validate-spike-file.mjs +1 -1
- package/scripts/refine/verify.mjs +11 -6
- package/scripts/release/assert-core-dependency-version.mjs +123 -0
- package/scripts/release/extract-changelog-section.mjs +16 -2
- package/skills/copilot-pr-followup/SKILL.md +60 -34
- package/skills/dev-loop/SKILL.md +11 -7
- package/skills/docs/acceptance-criteria-verification.md +8 -3
- package/skills/docs/anti-patterns.md +5 -3
- package/skills/docs/artifact-authority-contract.md +30 -11
- package/skills/docs/confirmation-rules.md +1 -0
- package/skills/docs/contract-style-guide.md +37 -0
- package/skills/docs/copilot-loop-operations.md +19 -15
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/debt-remediation-contract.md +1 -1
- package/skills/docs/epic-tree-refinement-procedure.md +25 -22
- package/skills/docs/issue-intake-procedure.md +26 -19
- package/skills/docs/local-planning-flow.md +1 -1
- package/skills/docs/local-planning-worked-example.md +1 -1
- package/skills/docs/merge-preconditions.md +30 -12
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/pr-lifecycle-contract.md +44 -35
- package/skills/docs/public-dev-loop-contract.md +60 -44
- package/skills/docs/required-rules.json +149 -0
- package/skills/docs/retrospective-checkpoint-contract.md +106 -79
- package/skills/docs/spike-mode-contract.md +14 -7
- package/skills/docs/stop-conditions.md +30 -15
- package/skills/docs/tracker-first-loop-state.md +13 -8
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/validation-policy.md +4 -5
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +62 -145
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/docs/validate-no-duplicate-rules.mjs +0 -250
- package/scripts/loop/conductor.mjs +0 -233
- package/scripts/loop/detect-stale-runner.mjs +0 -265
- 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, resolveGateAngleContract, resolveGateConfig, resolveLightMode, resolveRejectForeignAngles, resolveRequireFanoutEvidence, resolveRequireFanoutProvenance } from "@dev-loops/core/config";
|
|
18
|
+
import { FANOUT_UNAVAILABLE_MESSAGE, checkFanoutAngleCoverage, 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
|
|
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
|
}
|
|
@@ -232,6 +240,7 @@ function normalizeGateMarkerSummary(summary) {
|
|
|
232
240
|
}
|
|
233
241
|
export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, staleRunnerCheck = null, fanoutEnforcement = null) {
|
|
234
242
|
const failures = [];
|
|
243
|
+
const warnings = [];
|
|
235
244
|
if (!(evidence.draftGate.visible && evidence.draftGate.verdict === "clean")) {
|
|
236
245
|
failures.push("missing visible clean draft_gate comment");
|
|
237
246
|
}
|
|
@@ -249,7 +258,24 @@ export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, s
|
|
|
249
258
|
// { required: false, gates: [] } so the `.required` guard skips this block.
|
|
250
259
|
if (fanoutEnforcement && fanoutEnforcement.required) {
|
|
251
260
|
for (const gate of fanoutEnforcement.gates) {
|
|
252
|
-
|
|
261
|
+
// Light-mode acceptance (#1174): a genuinely under-threshold micro-PR
|
|
262
|
+
// collapses the gate fan-out to a single inline check (#1043). Accept that
|
|
263
|
+
// inline verdict ONLY when ALL hold, fail CLOSED otherwise:
|
|
264
|
+
// - lightMode is enabled in config, AND
|
|
265
|
+
// - the reviewed head's merge-base scope was RE-DERIVED under threshold
|
|
266
|
+
// (scopeUnderThreshold; false whenever scope could not be derived), AND
|
|
267
|
+
// - the PR carries no gate:full label (which always forces fan-out), AND
|
|
268
|
+
// - the verdict records a non-empty inline reason.
|
|
269
|
+
// Any non-light inline verdict (over threshold / label / lightMode off /
|
|
270
|
+
// scope underivable) falls through to the byte-identical rejection below.
|
|
271
|
+
const lightAccepted =
|
|
272
|
+
gate.executionMode === "inline_single_agent"
|
|
273
|
+
&& fanoutEnforcement.lightMode === true
|
|
274
|
+
&& fanoutEnforcement.hasFullLabel !== true
|
|
275
|
+
&& gate.scopeUnderThreshold === true
|
|
276
|
+
&& typeof gate.inlineReason === "string"
|
|
277
|
+
&& gate.inlineReason.trim().length > 0;
|
|
278
|
+
if (gate.executionMode !== "fanout_fanin" && !lightAccepted) {
|
|
253
279
|
failures.push(
|
|
254
280
|
`${gate.name}: requireFanoutEvidence is enabled but executionMode is "${gate.executionMode ?? "unset"}" (expected "fanout_fanin"); inline gate verdicts are not accepted`,
|
|
255
281
|
);
|
|
@@ -259,6 +285,67 @@ export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, s
|
|
|
259
285
|
failures.push(
|
|
260
286
|
`${gate.name}: requireFanoutEvidence is enabled but no findings-log ledger exists for the reviewed head (${gate.ledgerPath})`,
|
|
261
287
|
);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// Opt-in provenance enforcement (gates.requireFanoutProvenance), layered on
|
|
291
|
+
// top of fan-out evidence. When off (default) requireProvenance is falsy so
|
|
292
|
+
// NO new failure is added — behavior is byte-identical to today. When on, a
|
|
293
|
+
// fanout_fanin ledger must record INTERNALLY-CONSISTENT provenance (checked
|
|
294
|
+
// the same way the write path validates it — a hand-edited or shadow ledger
|
|
295
|
+
// is re-validated here, not trusted) with distinctReviewers >= the floor.
|
|
296
|
+
// Provenance is enforced ONLY for fanout_fanin verdicts (#1174): a
|
|
297
|
+
// light-accepted inline verdict is already scope-bounded and has no
|
|
298
|
+
// multi-reviewer provenance to record, so requiring it would make the
|
|
299
|
+
// light path unmergeable — the inverse of this issue's fix.
|
|
300
|
+
if (fanoutEnforcement.requireProvenance && gate.executionMode === "fanout_fanin") {
|
|
301
|
+
const prov = gate.provenance;
|
|
302
|
+
const consistencyErr = provenanceConsistencyError(prov);
|
|
303
|
+
const reviewers = prov && Number.isInteger(prov.distinctReviewers) ? prov.distinctReviewers : null;
|
|
304
|
+
if (consistencyErr) {
|
|
305
|
+
failures.push(
|
|
306
|
+
`${gate.name}: requireFanoutProvenance is enabled but the findings-log ledger lacks valid fan-out provenance (${consistencyErr}); ${FANOUT_UNAVAILABLE_MESSAGE}`,
|
|
307
|
+
);
|
|
308
|
+
} else if (reviewers === null || reviewers < FANOUT_PROVENANCE_MIN_REVIEWERS) {
|
|
309
|
+
failures.push(
|
|
310
|
+
`${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}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Angle-coverage enforcement: independent of requireFanoutProvenance.
|
|
315
|
+
// When the gate configures mandatory angles, a fanout_fanin ledger MUST
|
|
316
|
+
// record internally-consistent provenance — otherwise a shadow ledger
|
|
317
|
+
// that simply omits provenance would bypass mandatory-angle coverage.
|
|
318
|
+
// Recorded provenance is then re-validated: perAngle must cover every
|
|
319
|
+
// mandatory angle and (default) stay within the configured pool. Gates
|
|
320
|
+
// without mandatory angles keep today's behavior (absent provenance adds
|
|
321
|
+
// no failure; that stricter gap is requireFanoutProvenance's opt-in).
|
|
322
|
+
if (gate.executionMode === "fanout_fanin") {
|
|
323
|
+
const mandatoryAngles = gate.mandatoryAngles ?? [];
|
|
324
|
+
const provValid = gate.provenance != null && provenanceConsistencyError(gate.provenance) === null;
|
|
325
|
+
if (mandatoryAngles.length > 0 && !provValid) {
|
|
326
|
+
failures.push(
|
|
327
|
+
`${gate.name}: mandatory angle coverage is configured (${mandatoryAngles.join(", ")}) but the findings-log ledger records no valid fan-out provenance to verify it against; write the ledger with --provenance covering the mandatory angles; ${FANOUT_UNAVAILABLE_MESSAGE}`,
|
|
328
|
+
);
|
|
329
|
+
} else if (provValid) {
|
|
330
|
+
const { missingMandatory, foreignAngles } = checkFanoutAngleCoverage(gate.provenance.perAngle, {
|
|
331
|
+
mandatoryAngles,
|
|
332
|
+
pool: gate.anglePool ?? null,
|
|
333
|
+
});
|
|
334
|
+
if (missingMandatory.length > 0) {
|
|
335
|
+
failures.push(
|
|
336
|
+
`${gate.name}: fan-out provenance is missing mandatory angle(s): ${missingMandatory.join(", ")}; ${FANOUT_UNAVAILABLE_MESSAGE}`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (foreignAngles.length > 0) {
|
|
340
|
+
const message = `${gate.name}: fan-out provenance names angle(s) outside the configured pool: ${foreignAngles.join(", ")}`;
|
|
341
|
+
if (fanoutEnforcement.rejectForeignAngles ?? true) {
|
|
342
|
+
failures.push(`${message}; ${FANOUT_UNAVAILABLE_MESSAGE}`);
|
|
343
|
+
} else {
|
|
344
|
+
// rejectForeignAngles: false is WARNING mode, not silence.
|
|
345
|
+
warnings.push(`${message} (gates.rejectForeignAngles is false; recorded as a warning)`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
262
349
|
}
|
|
263
350
|
}
|
|
264
351
|
}
|
|
@@ -277,6 +364,8 @@ export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, s
|
|
|
277
364
|
return {
|
|
278
365
|
ok: failures.length === 0,
|
|
279
366
|
failures,
|
|
367
|
+
// Additive: only present when non-empty, preserving the existing {ok, failures} shape.
|
|
368
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
280
369
|
};
|
|
281
370
|
}
|
|
282
371
|
async function ledgerExists(fullPath) {
|
|
@@ -287,43 +376,163 @@ async function ledgerExists(fullPath) {
|
|
|
287
376
|
return false;
|
|
288
377
|
}
|
|
289
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* True if the ledger (relative path) exists under ANY enumerated checkout
|
|
381
|
+
* (main + every worktree). Fixes #1050: a ledger written in the PR worktree is
|
|
382
|
+
* found even when the check runs from a different checkout's git-toplevel.
|
|
383
|
+
*/
|
|
384
|
+
async function ledgerExistsInAny(checkouts, ledgerPath) {
|
|
385
|
+
for (const root of checkouts) {
|
|
386
|
+
if (await ledgerExists(path.resolve(root, ledgerPath))) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Read the recorded fan-out `provenance` object from a ledger across the
|
|
394
|
+
* enumerated checkouts. Mirrors ledgerExistsInAny's "ANY checkout satisfies"
|
|
395
|
+
* semantics: prefers the FIRST checkout whose ledger provenance actually
|
|
396
|
+
* SATISFIES the FULL active enforcement — internally consistent, meeting the
|
|
397
|
+
* distinctReviewers floor when requireFanoutProvenance is on, AND passing the
|
|
398
|
+
* gate's angle contract (mandatory-angle coverage; pool membership when
|
|
399
|
+
* foreign angles are rejected) — so a stale checkout's below-floor,
|
|
400
|
+
* provenance-less, or angle-contract-failing ledger cannot SHADOW a valid one
|
|
401
|
+
* in the PR worktree (which would falsely fail closed). Falls back to the
|
|
402
|
+
* first non-null provenance (for a useful diagnostic message) only when NO
|
|
403
|
+
* checkout satisfies, and null only when none is present. Called whenever
|
|
404
|
+
* requireFanoutProvenance is enabled OR the gate's verdict is fanout_fanin —
|
|
405
|
+
* inline verdicts never trigger this read.
|
|
406
|
+
*/
|
|
407
|
+
async function readLedgerProvenanceInAny(checkouts, ledgerPath, criteria = {}) {
|
|
408
|
+
const { requireProvenance = false, mandatoryAngles = [], anglePool = null, rejectForeignAngles = true } = criteria;
|
|
409
|
+
const satisfies = (prov) => {
|
|
410
|
+
if (provenanceConsistencyError(prov) !== null) return false;
|
|
411
|
+
if (requireProvenance && prov.distinctReviewers < FANOUT_PROVENANCE_MIN_REVIEWERS) return false;
|
|
412
|
+
const { missingMandatory, foreignAngles } = checkFanoutAngleCoverage(prov.perAngle, { mandatoryAngles, pool: anglePool });
|
|
413
|
+
if (missingMandatory.length > 0) return false;
|
|
414
|
+
if (foreignAngles.length > 0 && rejectForeignAngles) return false;
|
|
415
|
+
return true;
|
|
416
|
+
};
|
|
417
|
+
let firstNonNull = null;
|
|
418
|
+
for (const root of checkouts) {
|
|
419
|
+
const full = path.resolve(root, ledgerPath);
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(await readFile(full, "utf8"));
|
|
422
|
+
const prov = parsed && typeof parsed === "object" ? parsed.provenance : null;
|
|
423
|
+
if (prov == null) continue; // ledger present but no provenance — keep scanning.
|
|
424
|
+
if (satisfies(prov)) {
|
|
425
|
+
return prov; // satisfying ledger — prefer it over any earlier failing one.
|
|
426
|
+
}
|
|
427
|
+
if (firstNonNull === null) firstNonNull = prov; // remember for diagnostics.
|
|
428
|
+
} catch {
|
|
429
|
+
// Missing/unreadable/malformed ledger in this checkout — try the next.
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return firstNonNull;
|
|
433
|
+
}
|
|
290
434
|
/**
|
|
291
435
|
* Build the fan-out evidence enforcement descriptor.
|
|
292
436
|
*
|
|
293
437
|
* Enforcement is ON by default (opt-out via gates.requireFanoutEvidence: false).
|
|
294
438
|
* Returns { required: false } when enforcement is disabled OR when config is
|
|
295
439
|
* unavailable (config == null — null or undefined — after a failed load) — config-unavailable must
|
|
296
|
-
* fail open and never enable enforcement. When enabled,
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
440
|
+
* fail open and never enable enforcement. When enabled, returns
|
|
441
|
+
* { required: true, requireProvenance, lightMode, hasFullLabel, gates } where
|
|
442
|
+
* each per-required-gate entry records executionMode, inlineReason,
|
|
443
|
+
* scopeUnderThreshold, and whether the deterministic findings-log ledger exists
|
|
444
|
+
* for the reviewed head SHA, so the pre-merge check can fail closed on inline
|
|
445
|
+
* verdicts or missing ledgers.
|
|
446
|
+
*
|
|
447
|
+
* Light mode (#1174): when gates.lightMode is configured, `hasFullLabel`
|
|
448
|
+
* (gate:full PR label) and `baseRef` feed a fail-closed merge-base scope
|
|
449
|
+
* re-derivation for inline verdicts. A gate's scopeUnderThreshold is true only
|
|
450
|
+
* when light mode is on, the PR has no gate:full label, a base ref is known,
|
|
451
|
+
* and the reviewed head's merge-base diff is genuinely under threshold — which
|
|
452
|
+
* lets the pre-merge check accept an inline single-agent verdict for a
|
|
453
|
+
* small-scope PR instead of always rejecting inline evidence.
|
|
300
454
|
*/
|
|
301
|
-
async function buildFanoutEnforcement({ repo, pr, currentHeadSha, draftGateMarker, preApprovalGateMarker, config, cwd }) {
|
|
455
|
+
export async function buildFanoutEnforcement({ repo, pr, currentHeadSha, draftGateMarker, preApprovalGateMarker, config, cwd, hasFullLabel = false, baseRef = null }) {
|
|
302
456
|
// Fail open when config could not be loaded/validated. `== null` covers both
|
|
303
457
|
// null and undefined; the loader only ever yields null on failure, but the
|
|
304
458
|
// loose check defensively treats an absent config as unavailable.
|
|
305
459
|
if (config == null || !resolveRequireFanoutEvidence(config)) {
|
|
460
|
+
// Disabled/unavailable return is intentionally byte-identical to before
|
|
461
|
+
// (no requireProvenance key): buildPreMergeGateCheck only reads it inside
|
|
462
|
+
// the `required` block, so this preserves the exact existing shape.
|
|
306
463
|
return { required: false, gates: [] };
|
|
307
464
|
}
|
|
308
|
-
|
|
309
|
-
|
|
465
|
+
// Provenance enforcement is opt-in and layered ON TOP of fan-out evidence: it
|
|
466
|
+
// only takes effect while evidence enforcement (above) is active.
|
|
467
|
+
const requireProvenance = resolveRequireFanoutProvenance(config);
|
|
468
|
+
// Angle-coverage enforcement (mandatory angles + pool membership) is layered
|
|
469
|
+
// independently of requireProvenance: it re-validates whatever provenance a
|
|
470
|
+
// fanout_fanin ledger actually recorded, regardless of that opt-in flag.
|
|
471
|
+
const rejectForeignAngles = resolveRejectForeignAngles(config);
|
|
472
|
+
// Light-mode facts (#1174): the threshold that a re-derived merge-base scope
|
|
473
|
+
// must fall under for an inline verdict to be accepted. null when lightMode is
|
|
474
|
+
// disabled → no inline verdict can ever be accepted (scopeUnderThreshold stays
|
|
475
|
+
// false), preserving today's rejection.
|
|
476
|
+
const lightThreshold = resolveLightMode(config);
|
|
477
|
+
const lightMode = lightThreshold != null;
|
|
478
|
+
const draftGateConfig = resolveGateConfig(config, "draft");
|
|
479
|
+
const preApprovalGateConfig = resolveGateConfig(config, "preApproval");
|
|
480
|
+
// Shared angle-contract resolver (exclude-filtered mandatory angles +
|
|
481
|
+
// additive-aware pool) — the same contract the write paths enforce. The
|
|
482
|
+
// field names here (`mandatoryAngles`/`anglePool`) are exactly what
|
|
483
|
+
// buildPreMergeGateCheck reads off each gate entry.
|
|
484
|
+
const buildAngleFields = (gateKey) => {
|
|
485
|
+
const { mandatoryAngles, pool } = resolveGateAngleContract(config, gateKey);
|
|
486
|
+
return { mandatoryAngles, anglePool: pool };
|
|
487
|
+
};
|
|
488
|
+
const GATE_ANGLE_CONFIG = {
|
|
489
|
+
draft_gate: buildAngleFields("draft"),
|
|
490
|
+
pre_approval_gate: buildAngleFields("preApproval"),
|
|
491
|
+
};
|
|
310
492
|
const gateSpecs = [
|
|
311
|
-
{ name: "draft_gate", marker: draftGateMarker, required:
|
|
312
|
-
{ name: "pre_approval_gate", marker: preApprovalGateMarker, required:
|
|
493
|
+
{ name: "draft_gate", marker: draftGateMarker, required: draftGateConfig.required },
|
|
494
|
+
{ name: "pre_approval_gate", marker: preApprovalGateMarker, required: preApprovalGateConfig.required },
|
|
313
495
|
].filter((spec) => spec.required && spec.marker.visible);
|
|
314
496
|
const gates = [];
|
|
497
|
+
const checkouts = resolveLedgerCheckouts(cwd);
|
|
498
|
+
// checkouts[0] is always resolveRepoRoot(cwd) (resolveLedgerCheckouts adds it
|
|
499
|
+
// first, unconditionally, and never throws — it falls back to cwd on git
|
|
500
|
+
// failure) — reuse it instead of a second `git rev-parse --show-toplevel`.
|
|
501
|
+
const repoRoot = checkouts[0];
|
|
315
502
|
for (const spec of gateSpecs) {
|
|
316
503
|
const headSha = spec.marker.headSha ?? currentHeadSha;
|
|
317
504
|
const ledgerPath = buildLogPath({ repo, pr, gate: spec.name, headSha, tmpRoot: "tmp" });
|
|
318
|
-
|
|
505
|
+
// Re-derive scope FAIL-CLOSED for inline verdicts only (the fan-out default
|
|
506
|
+
// path pays no git I/O). scopeUnderThreshold is true ONLY when lightMode is
|
|
507
|
+
// on, the PR has no gate:full label, a base ref is known, and the merge-base
|
|
508
|
+
// diff for the reviewed head is genuinely under threshold. Any git/scope
|
|
509
|
+
// failure leaves it false, so the inline verdict is rejected exactly as today.
|
|
510
|
+
let scopeUnderThreshold = false;
|
|
511
|
+
if (lightMode && !hasFullLabel && baseRef && spec.marker.executionMode === "inline_single_agent") {
|
|
512
|
+
const scope = detectMergeBaseScope({ base: baseRef, head: headSha, cwd: repoRoot });
|
|
513
|
+
scopeUnderThreshold = scope.ok === true && isEligibleForLightMode(scope, lightThreshold);
|
|
514
|
+
}
|
|
515
|
+
// Read ledger provenance for ANY fanout_fanin gate (not just when
|
|
516
|
+
// requireProvenance is on): angle-coverage enforcement re-validates
|
|
517
|
+
// whatever provenance is recorded independently of that opt-in flag.
|
|
518
|
+
// The selection criteria mirror the full active enforcement so a stale
|
|
519
|
+
// checkout's contract-failing ledger cannot shadow a passing one.
|
|
520
|
+
const readProvenance = requireProvenance || spec.marker.executionMode === "fanout_fanin";
|
|
521
|
+
const angleFields = GATE_ANGLE_CONFIG[spec.name];
|
|
319
522
|
gates.push({
|
|
320
523
|
name: spec.name,
|
|
321
524
|
executionMode: spec.marker.executionMode ?? null,
|
|
525
|
+
inlineReason: spec.marker.inlineReason ?? null,
|
|
526
|
+
scopeUnderThreshold,
|
|
322
527
|
ledgerPath,
|
|
323
|
-
ledgerExists: await
|
|
528
|
+
ledgerExists: await ledgerExistsInAny(checkouts, ledgerPath),
|
|
529
|
+
provenance: readProvenance
|
|
530
|
+
? await readLedgerProvenanceInAny(checkouts, ledgerPath, { requireProvenance, rejectForeignAngles, ...angleFields })
|
|
531
|
+
: null,
|
|
532
|
+
...angleFields,
|
|
324
533
|
});
|
|
325
534
|
}
|
|
326
|
-
return { required: true, gates };
|
|
535
|
+
return { required: true, requireProvenance, rejectForeignAngles, lightMode, hasFullLabel, gates };
|
|
327
536
|
}
|
|
328
537
|
export async function detectCheckpointEvidence(options, { env = process.env, ghCommand = "gh", cwd = process.cwd() } = {}) {
|
|
329
538
|
const runnerOwnership = await ensureAsyncRunnerOwnership({
|
|
@@ -378,8 +587,31 @@ export async function detectCheckpointEvidence(options, { env = process.env, ghC
|
|
|
378
587
|
// treat it as config-unavailable and leave fan-out enforcement disabled
|
|
379
588
|
// (preserves default behavior). Other gate checks remain unaffected.
|
|
380
589
|
let config = null;
|
|
381
|
-
const { config: loadedConfig, errors: configErrors } = await loadDevLoopConfig({ repoRoot: cwd });
|
|
590
|
+
const { config: loadedConfig, errors: configErrors } = await loadDevLoopConfig({ repoRoot: resolveRepoRoot(cwd) });
|
|
382
591
|
config = Array.isArray(configErrors) && configErrors.length > 0 ? null : loadedConfig;
|
|
592
|
+
// Light-mode pre-merge facts (#1174): the base commit for the merge-base scope
|
|
593
|
+
// re-derivation and whether the PR forces full fan-out via the gate:full label.
|
|
594
|
+
// Fetched LAZILY — only when fan-out enforcement is active AND lightMode is on
|
|
595
|
+
// AND a gate actually recorded an inline verdict — so the common fan-out path
|
|
596
|
+
// (and every existing caller/test) makes NO extra gh call and stays unchanged.
|
|
597
|
+
let baseRef = null;
|
|
598
|
+
let hasFullLabel = false;
|
|
599
|
+
const anyInlineVerdict = [draftGateMarker, preApprovalGateMarker].some(
|
|
600
|
+
(marker) => marker.visible && marker.executionMode === "inline_single_agent",
|
|
601
|
+
);
|
|
602
|
+
if (config != null && resolveRequireFanoutEvidence(config) && resolveLightMode(config) != null && anyInlineVerdict) {
|
|
603
|
+
try {
|
|
604
|
+
const lightFacts = await runGhJson(["pr", "view", String(options.pr), "--repo", options.repo, "--json", "baseRefOid,labels"], { env, ghCommand });
|
|
605
|
+
baseRef = typeof lightFacts?.baseRefOid === "string" && lightFacts.baseRefOid.trim().length > 0
|
|
606
|
+
? lightFacts.baseRefOid.trim()
|
|
607
|
+
: null;
|
|
608
|
+
hasFullLabel = Array.isArray(lightFacts?.labels)
|
|
609
|
+
&& lightFacts.labels.some((label) => (typeof label === "string" ? label : label?.name) === GATE_FULL_LABEL);
|
|
610
|
+
} catch {
|
|
611
|
+
// Fail CLOSED: without the label/base facts we cannot safely accept an
|
|
612
|
+
// inline verdict, so leave baseRef null (scope underivable → rejected).
|
|
613
|
+
}
|
|
614
|
+
}
|
|
383
615
|
const fanoutEnforcement = await buildFanoutEnforcement({
|
|
384
616
|
repo: options.repo,
|
|
385
617
|
pr: options.pr,
|
|
@@ -388,6 +620,8 @@ export async function detectCheckpointEvidence(options, { env = process.env, ghC
|
|
|
388
620
|
preApprovalGateMarker,
|
|
389
621
|
config,
|
|
390
622
|
cwd,
|
|
623
|
+
hasFullLabel,
|
|
624
|
+
baseRef,
|
|
391
625
|
});
|
|
392
626
|
return {
|
|
393
627
|
ok: true,
|
|
@@ -458,7 +692,14 @@ async function main() {
|
|
|
458
692
|
process.exitCode = 1;
|
|
459
693
|
return;
|
|
460
694
|
}
|
|
461
|
-
|
|
695
|
+
// Warnings (e.g. foreign angles under gates.rejectForeignAngles: false) do
|
|
696
|
+
// not fail the check but must not pass silently. Suppressed under --silent.
|
|
697
|
+
if (Array.isArray(preMergeGateCheck.warnings) && !options.silent) {
|
|
698
|
+
for (const warning of preMergeGateCheck.warnings) {
|
|
699
|
+
process.stderr.write(`WARNING: ${warning}\n`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
process.exitCode = emitResult(output, { jq: options.jq, silent: options.silent });
|
|
462
703
|
} catch (error) {
|
|
463
704
|
if (error && typeof error === "object" && "staleRunner" in error && error.staleRunner) {
|
|
464
705
|
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": "..." }
|
|
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: {
|
|
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
|
-
|
|
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) => {
|