@dev-loops/core 1.0.3 → 1.0.4-pre.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.
- package/package.json +1 -1
- package/src/analysis/change-classifier.mjs +49 -28
- package/src/analysis/diff-analyzer.mjs +127 -31
- package/src/claude/asset-generation.mjs +36 -1
- package/src/claude/hook-decisions.mjs +117 -18
- package/src/config/config.mjs +198 -86
- package/src/config/extension-defaults.yaml +66 -6
- package/src/github/copilot-helpers.mjs +85 -20
- package/src/github/gh.mjs +14 -1
- package/src/github/review-threads.mjs +6 -0
- package/src/loop/bash-command-classify.mjs +251 -14
- package/src/loop/copilot-ci-status.mjs +116 -6
- package/src/loop/copilot-loop-state.mjs +38 -2
- package/src/loop/finding-cluster.mjs +30 -11
- package/src/loop/gate-carry-forward.mjs +39 -6
- package/src/loop/gate-fanin.mjs +58 -3
- package/src/loop/issue-refinement-artifact.mjs +117 -9
- package/src/loop/main-checkout-ff.mjs +169 -17
- package/src/loop/merge-approval.mjs +186 -5
- package/src/loop/pr-gate-coordination.mjs +339 -67
- package/src/loop/run-inspection.mjs +6 -0
- package/src/loop/spec-authority.mjs +40 -6
- package/src/loop/ui-e2e-scoping.mjs +1 -0
- package/src/projects/move-queue-item.mjs +5 -79
- package/src/projects/projects-access.mjs +124 -0
|
@@ -100,7 +100,19 @@ function normalizeGateComment(summary = null) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
// `unresolvedGateThreadCount` (ADR 0088): the gate-authored review-thread
|
|
104
|
+
// invariant close-gate-findings.mjs/ready-for-review.mjs already enforce
|
|
105
|
+
// (GATE-EXEC-FINDING-THREADS) — a clean marker verdict alone is not
|
|
106
|
+
// sufficient while a gate-authored thread (e.g. an unanswered or
|
|
107
|
+
// not-yet-judge-rejected `question`) still dangles. Folded ONLY into
|
|
108
|
+
// `currentHeadClean` here, never `cleanEvidenceExists`: `undefined`/`null`
|
|
109
|
+
// (the caller did not supply the signal — preApprovalGate's own
|
|
110
|
+
// toGateStatus call never does) preserves the marker-only definition
|
|
111
|
+
// exactly, so only draftGate's `currentHeadClean` — the one field that
|
|
112
|
+
// gates MARK_READY_FOR_REVIEW below — gains this guard. -1 (fail-closed
|
|
113
|
+
// "could not read thread state") counts as NOT clean, same as any positive
|
|
114
|
+
// count.
|
|
115
|
+
function toGateStatus(comment, marker, currentHeadSha, unresolvedGateThreadCount) {
|
|
104
116
|
const normalizedComment = normalizeGateComment(comment);
|
|
105
117
|
const normalizedMarker = normalizeGateComment(marker);
|
|
106
118
|
const markerHeadMatches = normalizedMarker.headSha !== null
|
|
@@ -109,6 +121,26 @@ function toGateStatus(comment, marker, currentHeadSha) {
|
|
|
109
121
|
const anyVisible = normalizedComment.visible || normalizedMarker.visible;
|
|
110
122
|
|
|
111
123
|
const cleanEvidenceExists = normalizedComment.visible && normalizedComment.verdict === "clean" && normalizedComment.headSha !== null;
|
|
124
|
+
// Only null/undefined means "the caller supplied no signal" (preApprovalGate's
|
|
125
|
+
// own toGateStatus call never passes this argument). Anything else that is
|
|
126
|
+
// not a non-negative integer — NaN, a float, a string, a negative number
|
|
127
|
+
// other than -1 — is a malformed signal from a future caller, treated the
|
|
128
|
+
// same as -1 (unreadable) rather than "no signal": this is a public core
|
|
129
|
+
// export, and failing open on a malformed count would let a broken caller
|
|
130
|
+
// report currentHeadClean=true with a dangling thread.
|
|
131
|
+
const hasThreadSignal = unresolvedGateThreadCount !== null && unresolvedGateThreadCount !== undefined;
|
|
132
|
+
const safeThreadCount = hasThreadSignal
|
|
133
|
+
? (Number.isInteger(unresolvedGateThreadCount) && unresolvedGateThreadCount >= 0 ? unresolvedGateThreadCount : -1)
|
|
134
|
+
: null;
|
|
135
|
+
const gateThreadsClean = !hasThreadSignal || safeThreadCount === 0;
|
|
136
|
+
// -1 is a distinct signal from a real positive count: it means the caller
|
|
137
|
+
// could not even READ the thread state (API failure) or supplied a
|
|
138
|
+
// malformed value, not that a thread is known to dangle. Both still block
|
|
139
|
+
// MARK_READY_FOR_REVIEW, but the two get different reasons below (a real
|
|
140
|
+
// thread names its own remedy; an unreadable count names retrying the read
|
|
141
|
+
// instead).
|
|
142
|
+
const gateThreadCountUnreadable = safeThreadCount === -1;
|
|
143
|
+
const markerVerdictClean = normalizedMarker.visible && markerHeadMatches && normalizedMarker.verdict === "clean" && normalizedMarker.contractComplete;
|
|
112
144
|
|
|
113
145
|
return {
|
|
114
146
|
visible: normalizedComment.visible,
|
|
@@ -120,7 +152,18 @@ function toGateStatus(comment, marker, currentHeadSha) {
|
|
|
120
152
|
findingsSummary: normalizedComment.findingsSummary ?? normalizedMarker.findingsSummary,
|
|
121
153
|
nextAction: normalizedComment.nextAction ?? normalizedMarker.nextAction,
|
|
122
154
|
contractComplete: normalizedMarker.visible && markerHeadMatches && normalizedMarker.contractComplete,
|
|
123
|
-
currentHeadClean:
|
|
155
|
+
currentHeadClean: markerVerdictClean && gateThreadsClean,
|
|
156
|
+
// ADR 0088: the marker verdict is clean on its own terms, but a
|
|
157
|
+
// gate-authored thread still dangles — distinct from "not clean at all"
|
|
158
|
+
// (draftGate below uses this to name the thread blocker directly rather
|
|
159
|
+
// than re-running the draft gate or blocking on CI, neither of which can
|
|
160
|
+
// ever clear a dangling thread). Excludes the -1 (unreadable) case below,
|
|
161
|
+
// which gets its own distinct reason.
|
|
162
|
+
markerCleanThreadsUnresolved: markerVerdictClean && !gateThreadsClean && !gateThreadCountUnreadable,
|
|
163
|
+
// The thread-state READ itself failed (-1) rather than reporting a real
|
|
164
|
+
// dangling thread — MARK_READY_FOR_REVIEW stays forbidden either way, but
|
|
165
|
+
// this names "retry the read" instead of "resolve a thread".
|
|
166
|
+
markerCleanThreadStateUnreadable: markerVerdictClean && gateThreadCountUnreadable,
|
|
124
167
|
cleanEvidenceExists,
|
|
125
168
|
};
|
|
126
169
|
}
|
|
@@ -592,7 +635,60 @@ function applyUnsettledCopilotReviewEntryGuard(input, result) {
|
|
|
592
635
|
const copilotReviewRequestStatus = typeof input.copilotReviewRequestStatus === "string"
|
|
593
636
|
? input.copilotReviewRequestStatus.trim().toLowerCase()
|
|
594
637
|
: "none";
|
|
595
|
-
|
|
638
|
+
const sameHeadCleanConverged = input.sameHeadCleanConverged === true;
|
|
639
|
+
const copilotReviewRoundCount = normalizeNonNegativeInteger(input.copilotReviewRoundCount);
|
|
640
|
+
const copilotReviewOnCurrentHead = input.copilotReviewOnCurrentHead === true;
|
|
641
|
+
const outstandingRequest = copilotReviewRequestStatus === "requested"
|
|
642
|
+
|| copilotReviewRequestStatus === "already-requested";
|
|
643
|
+
const roundCapReached = isCopilotRoundCapReached({
|
|
644
|
+
copilotReviewRoundCount: input.copilotReviewRoundCount,
|
|
645
|
+
maxCopilotRounds: input.maxCopilotRounds,
|
|
646
|
+
});
|
|
647
|
+
// Absent / never-driven: Copilot review is enabled (maxCopilotRounds
|
|
648
|
+
// > 0, not internal_only) but no round was requested or received for the
|
|
649
|
+
// CURRENT head, so a clean pre_approval_gate verdict would rest on nothing the
|
|
650
|
+
// loop actually drove on the head under review. The reconciled status alone is
|
|
651
|
+
// ambiguous: the reconciler (resolveCopilotReviewRequestStatus) folds a clean
|
|
652
|
+
// same-head submitted review into "none" too. Three facts each prove this is
|
|
653
|
+
// not the never-driven case and exempt this branch:
|
|
654
|
+
// - sameHeadCleanConverged: a clean same-head submitted Copilot review
|
|
655
|
+
// exists on THIS head (including GitHub's incidental auto-review) — the
|
|
656
|
+
// no-redundant-re-request case;
|
|
657
|
+
// - copilotReviewOnCurrentHead: a submitted Copilot review exists on THIS
|
|
658
|
+
// head (a settled current-head round; at a grant boundary its threads are
|
|
659
|
+
// already resolved). This is keyed on CURRENT-HEAD evidence, never a raw
|
|
660
|
+
// across-PR round count: copilotReviewRoundCount counts reviews on ANY
|
|
661
|
+
// head, so a PR with prior-head rounds and a NEW current head that carries
|
|
662
|
+
// no review (interpreter state low_signal_converged with
|
|
663
|
+
// copilotReviewOnCurrentHead false) must still fail closed;
|
|
664
|
+
// - roundCapReached: at/past the cap no further Copilot round can be driven,
|
|
665
|
+
// so requiring a current-head review is impossible to satisfy — the
|
|
666
|
+
// pre_approval_gate reviews the post-cap head. The core only reaches a
|
|
667
|
+
// grant boundary here after gating CI/threads, and a significant
|
|
668
|
+
// post-convergence change routes to a rerequest (a new cycle) before this
|
|
669
|
+
// guard runs, so this exemption cannot mask a genuinely-unreviewed change;
|
|
670
|
+
// - postConvergenceReviewSuppressed: the caller verified a carried
|
|
671
|
+
// convergence (or an operator suppression marker) for this head: no
|
|
672
|
+
// outstanding request, zero unresolved threads, and a provably docs-only
|
|
673
|
+
// or integrate-only delta since Copilot's last submitted review, so the
|
|
674
|
+
// prior converged review stands for this head (never derived here from
|
|
675
|
+
// other snapshot facts).
|
|
676
|
+
// Fail closed on ANY non-outstanding status, not only the literal "none":
|
|
677
|
+
// this is the independent gate-ENTRY re-check, so it must not trust the
|
|
678
|
+
// caller's status string. A non-canonical/unknown value ("", "unavailable",
|
|
679
|
+
// "failed", a typo) with a grant-y lifecycleState and no current-head review
|
|
680
|
+
// fails closed rather than slipping through the "none"-only default. Only a
|
|
681
|
+
// driven current-head review (or the impossible-further-round cap state, or an
|
|
682
|
+
// operator-verified pure-doc-bump suppression) exempts, so an agent that skips
|
|
683
|
+
// the explicit Copilot round cannot reach a clean pre_approval verdict via any
|
|
684
|
+
// grant-y lifecycleState (e.g. a stale/racy low_signal_converged label, or one
|
|
685
|
+
// carried by prior-head rounds).
|
|
686
|
+
const absentNeverDriven = !outstandingRequest
|
|
687
|
+
&& !copilotReviewOnCurrentHead
|
|
688
|
+
&& !sameHeadCleanConverged
|
|
689
|
+
&& !roundCapReached
|
|
690
|
+
&& input.postConvergenceReviewSuppressed !== true;
|
|
691
|
+
if (!outstandingRequest && !absentNeverDriven) {
|
|
596
692
|
return null;
|
|
597
693
|
}
|
|
598
694
|
// Round-cap exemption (mirrors shouldGuardCopilotReviewRequest):
|
|
@@ -603,10 +699,6 @@ function applyUnsettledCopilotReviewEntryGuard(input, result) {
|
|
|
603
699
|
// and the head is clean — either sameHeadCleanConverged or the interpreter's
|
|
604
700
|
// round_cap_clean_fallback state — the pre_approval_gate proceeds unless
|
|
605
701
|
// significant post-convergence changes require a new review cycle.
|
|
606
|
-
const roundCapReached = isCopilotRoundCapReached({
|
|
607
|
-
copilotReviewRoundCount: input.copilotReviewRoundCount,
|
|
608
|
-
maxCopilotRounds: input.maxCopilotRounds,
|
|
609
|
-
});
|
|
610
702
|
const lifecycleState = typeof input.lifecycleState === "string" ? input.lifecycleState.trim().toLowerCase() : "";
|
|
611
703
|
// Also exempt the evaluator's own ROUND_CAP_REACHED grant shape:
|
|
612
704
|
// without this, this guard would rewrite that grant back to
|
|
@@ -624,7 +716,15 @@ function applyUnsettledCopilotReviewEntryGuard(input, result) {
|
|
|
624
716
|
|
|
625
717
|
const allowedNextActions = [];
|
|
626
718
|
const forbiddenActions = [];
|
|
627
|
-
|
|
719
|
+
// Two shapes fail closed here. An OUTSTANDING request (requested/
|
|
720
|
+
// already-requested) waits for the current-head review to settle. The
|
|
721
|
+
// ABSENT / never-driven case has nothing to wait for — no request is
|
|
722
|
+
// in flight — so the loop must first REQUEST a Copilot review, mirroring the
|
|
723
|
+
// detector-side shouldGuardCopilotReviewRequest override.
|
|
724
|
+
const nextAction = absentNeverDriven
|
|
725
|
+
? PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW
|
|
726
|
+
: PR_CHECKPOINT_ACTION.WAIT_FOR_COPILOT_REVIEW;
|
|
727
|
+
pushUnique(allowedNextActions, [nextAction]);
|
|
628
728
|
// Full postDraftForbidden set (matching the canonical WAITING_FOR_COPILOT_REVIEW
|
|
629
729
|
// result this guard synthesizes) plus the final-approval actions the replaced
|
|
630
730
|
// boundary result also forbade — dropping RUN_DRAFT_GATE/MARK_READY_FOR_REVIEW
|
|
@@ -642,18 +742,24 @@ function applyUnsettledCopilotReviewEntryGuard(input, result) {
|
|
|
642
742
|
repo: input.repo ?? null,
|
|
643
743
|
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
644
744
|
currentHeadSha: result.currentHeadSha ?? null,
|
|
645
|
-
lifecycleState: STATE.WAITING_FOR_COPILOT_REVIEW,
|
|
646
|
-
loopDisposition: DISPOSITION.PENDING,
|
|
745
|
+
lifecycleState: absentNeverDriven ? STATE.PR_READY_NO_FEEDBACK : STATE.WAITING_FOR_COPILOT_REVIEW,
|
|
746
|
+
loopDisposition: absentNeverDriven ? DISPOSITION.ACTION_REQUIRED : DISPOSITION.PENDING,
|
|
647
747
|
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
648
748
|
draftGateAlreadySatisfied: result.draftGateAlreadySatisfied === true,
|
|
649
749
|
draftGate: result.draftGate,
|
|
650
750
|
preApprovalGate: result.preApprovalGate,
|
|
651
751
|
allowedNextActions,
|
|
652
752
|
forbiddenActions,
|
|
653
|
-
nextAction
|
|
654
|
-
reason:
|
|
655
|
-
|
|
656
|
-
|
|
753
|
+
nextAction,
|
|
754
|
+
reason: absentNeverDriven
|
|
755
|
+
? "Copilot review is enabled for this repo (maxCopilotRounds > 0) but no Copilot review round has been "
|
|
756
|
+
+ "requested or received for the current head (independent gate-entry re-check, issue #2146) — a clean "
|
|
757
|
+
+ "pre_approval_gate/final-approval verdict requires a Copilot round the loop actually drove and awaited, "
|
|
758
|
+
+ "so request Copilot review first. A clean same-head submitted Copilot review (including GitHub's "
|
|
759
|
+
+ "auto-review) would satisfy this; an absent/never-driven round is not settled convergence."
|
|
760
|
+
: "A Copilot review request is still outstanding on the current head (independent gate-entry "
|
|
761
|
+
+ "re-check, issue #1190) — pre_approval_gate/final-approval entry is refused until the current-head "
|
|
762
|
+
+ "review settles, even though the caller-reported convergence signal claims otherwise.",
|
|
657
763
|
mergeStateStatus: result.mergeStateStatus ?? null,
|
|
658
764
|
conflictFiles: result.conflictFiles ?? [],
|
|
659
765
|
refinementArtifact: result.refinementArtifact ?? null,
|
|
@@ -661,6 +767,67 @@ function applyUnsettledCopilotReviewEntryGuard(input, result) {
|
|
|
661
767
|
});
|
|
662
768
|
}
|
|
663
769
|
|
|
770
|
+
/**
|
|
771
|
+
* Boundaries at which a PR must carry clean draft_gate evidence; without it
|
|
772
|
+
* the only legal next action is reconcile_draft_gate
|
|
773
|
+
* (skills/docs/pr-lifecycle-contract.md, LIFECYCLE-FAIL-CLOSED).
|
|
774
|
+
*/
|
|
775
|
+
const DRAFT_GATE_EVIDENCE_GUARDED_BOUNDARIES = Object.freeze([
|
|
776
|
+
PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
777
|
+
PR_CHECKPOINT.FEEDBACK_RESOLUTION,
|
|
778
|
+
PR_CHECKPOINT.PRE_APPROVAL_GATE_NEEDED,
|
|
779
|
+
PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
780
|
+
PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
781
|
+
]);
|
|
782
|
+
|
|
783
|
+
function applyDraftGateEvidenceGuard(result, { prDraft = false } = {}) {
|
|
784
|
+
if (!result || typeof result !== "object") {
|
|
785
|
+
return result;
|
|
786
|
+
}
|
|
787
|
+
// The PR is still draft: absent/not-yet-clean draft_gate evidence is the
|
|
788
|
+
// EXPECTED state while draft_gate remediation is in progress (e.g. a fixer
|
|
789
|
+
// mid-way through COMPLETE_FIXER_DISPOSITION for threads it tackled from an
|
|
790
|
+
// earlier draft_gate round) — the PR_DRAFT branch above already owns this
|
|
791
|
+
// state (RUN_DRAFT_GATE / MARK_READY_FOR_REVIEW). This guard exists for the
|
|
792
|
+
// POST-draft merge-path boundaries only (mirrors the wrapper's own
|
|
793
|
+
// prDraft skip for the title-marker guard, below); rewriting an in-progress
|
|
794
|
+
// draft-side result to reconcile_draft_gate would misdirect the fixer.
|
|
795
|
+
if (prDraft) {
|
|
796
|
+
return result;
|
|
797
|
+
}
|
|
798
|
+
if (result.draftGate?.cleanEvidenceExists === true) {
|
|
799
|
+
return result;
|
|
800
|
+
}
|
|
801
|
+
if (!DRAFT_GATE_EVIDENCE_GUARDED_BOUNDARIES.includes(result.gateBoundary)) {
|
|
802
|
+
return result;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const allowedNextActions = [];
|
|
806
|
+
const forbiddenActions = [];
|
|
807
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.RECONCILE_DRAFT_GATE]);
|
|
808
|
+
pushUnique(forbiddenActions, [
|
|
809
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
810
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
811
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
812
|
+
PR_CHECKPOINT_ACTION.WAIT_FOR_COPILOT_REVIEW,
|
|
813
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
814
|
+
PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
815
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
816
|
+
]);
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
...result,
|
|
820
|
+
gateBoundary: PR_CHECKPOINT.DRAFT_GATE_NEEDED,
|
|
821
|
+
nextAction: PR_CHECKPOINT_ACTION.RECONCILE_DRAFT_GATE,
|
|
822
|
+
allowedNextActions,
|
|
823
|
+
forbiddenActions,
|
|
824
|
+
reason: result.draftGate?.anyVisible
|
|
825
|
+
? "Clean draft_gate evidence is required before merge (no gate exemptions, #579). A draft_gate comment exists but is not clean; convert the PR back to draft before re-running draft_gate, or clear the existing evidence before running reconcile_draft_gate."
|
|
826
|
+
: "Clean draft_gate evidence is required before merge (no gate exemptions, #579). No visible clean draft_gate comment exists for this PR; run reconcile_draft_gate before proceeding.",
|
|
827
|
+
gateEvidenceNote: null,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
664
831
|
/**
|
|
665
832
|
* Evaluates PR gate coordination, then re-asserts the merge-blocking title guard
|
|
666
833
|
* at the pre-approval / final-approval boundary for non-draft PRs.
|
|
@@ -669,31 +836,38 @@ function applyUnsettledCopilotReviewEntryGuard(input, result) {
|
|
|
669
836
|
* sites (defense in depth); this wrapper additionally covers the pre-approval
|
|
670
837
|
* gate boundary, which is reached before any pre-approval evidence exists and so
|
|
671
838
|
* is not protected by the inline checks.
|
|
839
|
+
*
|
|
840
|
+
* Invariant: every return path passes through `applyDraftGateEvidenceGuard`,
|
|
841
|
+
* which rewrites the five DRAFT_GATE_EVIDENCE_GUARDED_BOUNDARIES boundaries to
|
|
842
|
+
* DRAFT_GATE_NEEDED / reconcile_draft_gate whenever clean draft_gate evidence
|
|
843
|
+
* is absent — a still-draft PR is exempted (ADR 0088): absent/not-yet-clean
|
|
844
|
+
* draft_gate evidence is the normal state while draft_gate remediation is in
|
|
845
|
+
* progress, and the PR_DRAFT branch already owns that state.
|
|
672
846
|
*/
|
|
673
847
|
export function evaluatePrGateCoordination(input = {}) {
|
|
674
848
|
const result = evaluatePrGateCoordinationCore(input);
|
|
849
|
+
const prDraft = input.prDraft === true;
|
|
675
850
|
|
|
676
851
|
const unsettledReviewResult = applyUnsettledCopilotReviewEntryGuard(input, result);
|
|
677
852
|
if (unsettledReviewResult) {
|
|
678
|
-
return unsettledReviewResult;
|
|
853
|
+
return applyDraftGateEvidenceGuard(unsettledReviewResult, { prDraft });
|
|
679
854
|
}
|
|
680
855
|
|
|
681
|
-
const prDraft = input.prDraft === true;
|
|
682
856
|
const prTitle = typeof input.prTitle === "string" ? input.prTitle : "";
|
|
683
857
|
// Draft PRs may legitimately carry a WIP title; the marker only blocks once
|
|
684
858
|
// the PR has left draft and is at a pre-approval/final-approval boundary.
|
|
685
859
|
if (prDraft || !result || typeof result !== "object") {
|
|
686
|
-
return result;
|
|
860
|
+
return applyDraftGateEvidenceGuard(result, { prDraft });
|
|
687
861
|
}
|
|
688
862
|
if (!TITLE_MARKER_GUARDED_BOUNDARIES.includes(result.gateBoundary)) {
|
|
689
|
-
return result;
|
|
863
|
+
return applyDraftGateEvidenceGuard(result, { prDraft });
|
|
690
864
|
}
|
|
691
865
|
const markers = findBlockingTitleMarkers(prTitle);
|
|
692
866
|
if (markers.length === 0) {
|
|
693
|
-
return result;
|
|
867
|
+
return applyDraftGateEvidenceGuard(result, { prDraft });
|
|
694
868
|
}
|
|
695
869
|
|
|
696
|
-
return buildTitleMarkerBlockedResult({
|
|
870
|
+
return applyDraftGateEvidenceGuard(buildTitleMarkerBlockedResult({
|
|
697
871
|
input,
|
|
698
872
|
currentHeadSha: result.currentHeadSha ?? null,
|
|
699
873
|
draftGateAlreadySatisfied: result.draftGateAlreadySatisfied === true,
|
|
@@ -703,7 +877,7 @@ export function evaluatePrGateCoordination(input = {}) {
|
|
|
703
877
|
conflictFiles: result.conflictFiles ?? [],
|
|
704
878
|
markers,
|
|
705
879
|
refinementArtifact: result.refinementArtifact ?? null,
|
|
706
|
-
});
|
|
880
|
+
}), { prDraft });
|
|
707
881
|
}
|
|
708
882
|
|
|
709
883
|
function evaluatePrGateCoordinationCore(input = {}) {
|
|
@@ -713,14 +887,15 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
713
887
|
const prDraft = input.prDraft === true;
|
|
714
888
|
const prClosed = input.prClosed === true;
|
|
715
889
|
const prMerged = input.prMerged === true;
|
|
716
|
-
const
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
890
|
+
const copilotConvergenceOk = input.copilotConvergenceOk === true;
|
|
891
|
+
// Carried convergence: set only when the caller verified, through the
|
|
892
|
+
// shared carried-convergence predicate the Copilot request tool also uses,
|
|
893
|
+
// that the prior converged Copilot review still stands for this head (no
|
|
894
|
+
// outstanding request, zero unresolved threads, a provably docs-only or
|
|
895
|
+
// integrate-only delta), or that an operator suppression marker re-verifies
|
|
896
|
+
// on the same terms. Never derived here from other snapshot facts: this
|
|
897
|
+
// evaluator trusts the caller's verification, so the request tool and the
|
|
898
|
+
// gate cannot disagree about the same head.
|
|
724
899
|
const postConvergenceReviewSuppressed = input.postConvergenceReviewSuppressed === true;
|
|
725
900
|
// maxCopilotRounds: 0 disables the external Copilot review gate entirely
|
|
726
901
|
// (for repos without Copilot / local-harness-only review). It reuses the
|
|
@@ -771,9 +946,20 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
771
946
|
const refinementArtifactStatus = normalizeRefinementArtifactStatus(refinementArtifact?.status);
|
|
772
947
|
const refinementLinkedIssue = Number.isInteger(refinementArtifact?.linkedIssue) ? refinementArtifact.linkedIssue : null;
|
|
773
948
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
949
|
+
// Keep the shared loop's body-feedback signal intact. Gate entry alone may
|
|
950
|
+
// reinterpret a thread-clean, current-head review that the shared convergence
|
|
951
|
+
// evaluator accepts (notably a conductor-overridable blue review).
|
|
952
|
+
const gateEntryConverged = copilotConvergenceOk
|
|
953
|
+
&& input.copilotReviewOnCurrentHead === true
|
|
954
|
+
&& unresolvedThreadCount === 0;
|
|
955
|
+
const effectiveLifecycleState = gateEntryConverged && lifecycleState === STATE.UNRESOLVED_FEEDBACK_PRESENT
|
|
956
|
+
? STATE.READY_TO_REREQUEST_REVIEW
|
|
957
|
+
: (gateEntryConverged && lifecycleState === STATE.ROUND_CAP_REACHED
|
|
958
|
+
? STATE.ROUND_CAP_CLEAN_FALLBACK
|
|
959
|
+
: lifecycleState);
|
|
960
|
+
const sameHeadCleanConverged = input.sameHeadCleanConverged === true || gateEntryConverged;
|
|
961
|
+
|
|
962
|
+
const draftGate = toGateStatus(input.draftGate, input.draftGateMarker, currentHeadSha, input.unresolvedGateThreadCount);
|
|
777
963
|
const preApprovalGate = toGateStatus(input.preApprovalGate, input.preApprovalGateMarker, currentHeadSha);
|
|
778
964
|
const draftGateAlreadySatisfied = !prDraft && (draftGate?.cleanEvidenceExists ?? false);
|
|
779
965
|
|
|
@@ -1056,7 +1242,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1056
1242
|
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
1057
1243
|
loopDisposition: DISPOSITION.BLOCKED,
|
|
1058
1244
|
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
1059
|
-
draftGateAlreadySatisfied
|
|
1245
|
+
draftGateAlreadySatisfied,
|
|
1060
1246
|
draftGate,
|
|
1061
1247
|
preApprovalGate,
|
|
1062
1248
|
allowedNextActions,
|
|
@@ -1076,6 +1262,83 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1076
1262
|
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
1077
1263
|
];
|
|
1078
1264
|
|
|
1265
|
+
// ADR 0088: the round's draft_gate marker verdict is ALREADY clean — CI
|
|
1266
|
+
// pending/failing or re-running draft_gate can never clear a dangling
|
|
1267
|
+
// gate-authored thread (e.g. an answered, judge-rejected `question`
|
|
1268
|
+
// close-gate-findings has not yet reject-closed), so this must run BEFORE
|
|
1269
|
+
// the CI-status checks below: neither a CI-blocked nor a run_draft_gate
|
|
1270
|
+
// result would name the real blocker, and a loop that followed
|
|
1271
|
+
// run_draft_gate would re-run a gate round that already passed.
|
|
1272
|
+
// reply_resolve_review_threads (FEEDBACK_RESOLUTION) is reused as-is —
|
|
1273
|
+
// resolving the thread (reject-close or fixer triage) is exactly what
|
|
1274
|
+
// this action already means on the post-draft path.
|
|
1275
|
+
if (draftGate.markerCleanThreadsUnresolved) {
|
|
1276
|
+
pushUnique(allowedNextActions, [
|
|
1277
|
+
PR_CHECKPOINT_ACTION.REPLY_RESOLVE_REVIEW_THREADS,
|
|
1278
|
+
// The reason text names "stop for the operator" as the remedy for an
|
|
1279
|
+
// unanswered question that cannot be answered — that escalation must
|
|
1280
|
+
// be a legal next action, not just prose an allowedNextActions-honoring
|
|
1281
|
+
// conductor has no sanctioned way to take.
|
|
1282
|
+
PR_CHECKPOINT_ACTION.REPORT_BLOCKED,
|
|
1283
|
+
]);
|
|
1284
|
+
pushUnique(forbiddenActions, [
|
|
1285
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
1286
|
+
...draftReviewForbidden,
|
|
1287
|
+
]);
|
|
1288
|
+
return buildResult({
|
|
1289
|
+
repo: input.repo ?? null,
|
|
1290
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1291
|
+
currentHeadSha,
|
|
1292
|
+
lifecycleState: lifecycleState || STATE.PR_DRAFT,
|
|
1293
|
+
loopDisposition: DISPOSITION.UNRESOLVED_FEEDBACK,
|
|
1294
|
+
gateBoundary: PR_CHECKPOINT.FEEDBACK_RESOLUTION,
|
|
1295
|
+
draftGateAlreadySatisfied,
|
|
1296
|
+
draftGate,
|
|
1297
|
+
preApprovalGate,
|
|
1298
|
+
allowedNextActions,
|
|
1299
|
+
forbiddenActions,
|
|
1300
|
+
nextAction: PR_CHECKPOINT_ACTION.REPLY_RESOLVE_REVIEW_THREADS,
|
|
1301
|
+
reason: "The PR is still draft and this round's draft_gate verdict is clean, but a gate-authored review thread is still unresolved (ADR 0088) — reply/resolve it rather than re-running the draft gate. An open defect thread is resolved via fixer fix-close, or the disposition pass's defer-close (close-gate-findings) once eligible. A question thread's remedy depends on the judge's disposition: unanswered — post an answer reply, then rerun close-gate-findings, which reject-closes it if the judge disposed it reject; answered and judge-disposed reject — rerun close-gate-findings to reject-close it; answered and judge-disposed act — resolved by the fixer's own answer-and-resolve path; judge-deferred, or ambiguous/unrecorded disposition — needs an operator decision, no automated path.",
|
|
1302
|
+
mergeStateStatus,
|
|
1303
|
+
conflictFiles,
|
|
1304
|
+
refinementArtifact,
|
|
1305
|
+
copilotReviewRoundCount,
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// Distinct from markerCleanThreadsUnresolved above: the thread-state READ
|
|
1310
|
+
// itself failed (unresolvedGateThreadCount === -1), not a real dangling
|
|
1311
|
+
// thread — naming "resolve the thread" would be misleading when there is
|
|
1312
|
+
// no known thread to resolve. MARK_READY_FOR_REVIEW stays forbidden
|
|
1313
|
+
// either way (draftReviewForbidden); reply_resolve_review_threads is
|
|
1314
|
+
// reused as-is rather than adding a new action for "retry the read".
|
|
1315
|
+
if (draftGate.markerCleanThreadStateUnreadable) {
|
|
1316
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPLY_RESOLVE_REVIEW_THREADS]);
|
|
1317
|
+
pushUnique(forbiddenActions, [
|
|
1318
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
1319
|
+
...draftReviewForbidden,
|
|
1320
|
+
]);
|
|
1321
|
+
return buildResult({
|
|
1322
|
+
repo: input.repo ?? null,
|
|
1323
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1324
|
+
currentHeadSha,
|
|
1325
|
+
lifecycleState: lifecycleState || STATE.PR_DRAFT,
|
|
1326
|
+
loopDisposition: DISPOSITION.UNRESOLVED_FEEDBACK,
|
|
1327
|
+
gateBoundary: PR_CHECKPOINT.FEEDBACK_RESOLUTION,
|
|
1328
|
+
draftGateAlreadySatisfied,
|
|
1329
|
+
draftGate,
|
|
1330
|
+
preApprovalGate,
|
|
1331
|
+
allowedNextActions,
|
|
1332
|
+
forbiddenActions,
|
|
1333
|
+
nextAction: PR_CHECKPOINT_ACTION.REPLY_RESOLVE_REVIEW_THREADS,
|
|
1334
|
+
reason: "The PR is still draft and this round's draft_gate verdict is clean, but this round's gate-authored review-thread count could not be read (API failure) — could not read review-thread state; re-run when API connectivity is restored, rather than treating an unreadable count as either clean or a real dangling thread.",
|
|
1335
|
+
mergeStateStatus,
|
|
1336
|
+
conflictFiles,
|
|
1337
|
+
refinementArtifact,
|
|
1338
|
+
copilotReviewRoundCount,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1079
1342
|
if (!draftGate.currentHeadClean && draftGateRequireCi) {
|
|
1080
1343
|
if (ciStatus === "failure") {
|
|
1081
1344
|
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_BLOCKED]);
|
|
@@ -1090,7 +1353,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1090
1353
|
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
1091
1354
|
loopDisposition: DISPOSITION.BLOCKED,
|
|
1092
1355
|
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
1093
|
-
draftGateAlreadySatisfied
|
|
1356
|
+
draftGateAlreadySatisfied,
|
|
1094
1357
|
draftGate,
|
|
1095
1358
|
preApprovalGate,
|
|
1096
1359
|
allowedNextActions,
|
|
@@ -1116,7 +1379,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1116
1379
|
lifecycleState: STATE.WAITING_FOR_CI,
|
|
1117
1380
|
loopDisposition: DISPOSITION.PENDING,
|
|
1118
1381
|
gateBoundary: PR_CHECKPOINT.DRAFT_REVIEW,
|
|
1119
|
-
draftGateAlreadySatisfied
|
|
1382
|
+
draftGateAlreadySatisfied,
|
|
1120
1383
|
draftGate,
|
|
1121
1384
|
preApprovalGate,
|
|
1122
1385
|
allowedNextActions,
|
|
@@ -1188,7 +1451,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1188
1451
|
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
1189
1452
|
loopDisposition: DISPOSITION.BLOCKED,
|
|
1190
1453
|
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
1191
|
-
draftGateAlreadySatisfied
|
|
1454
|
+
draftGateAlreadySatisfied,
|
|
1192
1455
|
draftGate,
|
|
1193
1456
|
preApprovalGate,
|
|
1194
1457
|
allowedNextActions,
|
|
@@ -1208,7 +1471,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1208
1471
|
return buildTitleMarkerBlockedResult({
|
|
1209
1472
|
input,
|
|
1210
1473
|
currentHeadSha,
|
|
1211
|
-
draftGateAlreadySatisfied
|
|
1474
|
+
draftGateAlreadySatisfied,
|
|
1212
1475
|
draftGate,
|
|
1213
1476
|
preApprovalGate,
|
|
1214
1477
|
mergeStateStatus,
|
|
@@ -1240,7 +1503,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1240
1503
|
lifecycleState: effectiveLifecycleState,
|
|
1241
1504
|
loopDisposition: loopDisposition ?? DISPOSITION.CLEAN_CONVERGED,
|
|
1242
1505
|
gateBoundary: PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
1243
|
-
draftGateAlreadySatisfied
|
|
1506
|
+
draftGateAlreadySatisfied,
|
|
1244
1507
|
draftGate,
|
|
1245
1508
|
preApprovalGate,
|
|
1246
1509
|
allowedNextActions,
|
|
@@ -1264,7 +1527,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1264
1527
|
lifecycleState: effectiveLifecycleState,
|
|
1265
1528
|
loopDisposition: loopDisposition ?? DISPOSITION.ACTION_REQUIRED,
|
|
1266
1529
|
gateBoundary: PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
1267
|
-
draftGateAlreadySatisfied
|
|
1530
|
+
draftGateAlreadySatisfied,
|
|
1268
1531
|
draftGate,
|
|
1269
1532
|
preApprovalGate,
|
|
1270
1533
|
allowedNextActions,
|
|
@@ -1391,7 +1654,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1391
1654
|
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
1392
1655
|
loopDisposition: DISPOSITION.BLOCKED,
|
|
1393
1656
|
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
1394
|
-
draftGateAlreadySatisfied
|
|
1657
|
+
draftGateAlreadySatisfied,
|
|
1395
1658
|
draftGate,
|
|
1396
1659
|
preApprovalGate,
|
|
1397
1660
|
allowedNextActions,
|
|
@@ -1416,7 +1679,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1416
1679
|
lifecycleState: STATE.WAITING_FOR_CI,
|
|
1417
1680
|
loopDisposition: DISPOSITION.PENDING,
|
|
1418
1681
|
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
1419
|
-
draftGateAlreadySatisfied
|
|
1682
|
+
draftGateAlreadySatisfied,
|
|
1420
1683
|
draftGate,
|
|
1421
1684
|
preApprovalGate,
|
|
1422
1685
|
allowedNextActions,
|
|
@@ -1450,7 +1713,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1450
1713
|
lifecycleState: effectiveLifecycleState,
|
|
1451
1714
|
loopDisposition: loopDisposition ?? DISPOSITION.ACTION_REQUIRED,
|
|
1452
1715
|
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
1453
|
-
draftGateAlreadySatisfied
|
|
1716
|
+
draftGateAlreadySatisfied,
|
|
1454
1717
|
draftGate,
|
|
1455
1718
|
preApprovalGate,
|
|
1456
1719
|
allowedNextActions,
|
|
@@ -1471,7 +1734,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1471
1734
|
return buildTitleMarkerBlockedResult({
|
|
1472
1735
|
input,
|
|
1473
1736
|
currentHeadSha,
|
|
1474
|
-
draftGateAlreadySatisfied
|
|
1737
|
+
draftGateAlreadySatisfied,
|
|
1475
1738
|
draftGate,
|
|
1476
1739
|
preApprovalGate,
|
|
1477
1740
|
mergeStateStatus,
|
|
@@ -1481,7 +1744,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1481
1744
|
});
|
|
1482
1745
|
}
|
|
1483
1746
|
|
|
1484
|
-
if (!draftGate.cleanEvidenceExists
|
|
1747
|
+
if (!draftGate.cleanEvidenceExists) {
|
|
1485
1748
|
return buildDraftGateNeededForMergeResult({
|
|
1486
1749
|
input,
|
|
1487
1750
|
currentHeadSha,
|
|
@@ -1509,14 +1772,14 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1509
1772
|
lifecycleState: effectiveLifecycleState,
|
|
1510
1773
|
loopDisposition: loopDisposition ?? DISPOSITION.CLEAN_CONVERGED,
|
|
1511
1774
|
gateBoundary: PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
1512
|
-
draftGateAlreadySatisfied
|
|
1775
|
+
draftGateAlreadySatisfied,
|
|
1513
1776
|
draftGate,
|
|
1514
1777
|
preApprovalGate,
|
|
1515
1778
|
allowedNextActions,
|
|
1516
1779
|
forbiddenActions,
|
|
1517
1780
|
nextAction: PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
1518
1781
|
reason: roundCapReached
|
|
1519
|
-
? `Round-cap clean fallback
|
|
1782
|
+
? `Round-cap clean fallback (${copilotReviewRoundCount}/${maxCopilotRounds} rounds, zero unresolved threads, ${describeAcceptedCiState(ciStatus, preApprovalRequireCi)}) with clean draft_gate evidence; the current head has clean \`pre_approval_gate\` evidence, so the PR is at the final approval boundary.`
|
|
1520
1783
|
: (ciStatus === "crediblyGreen"
|
|
1521
1784
|
? "The current head has both a clean settled review cycle and clean `pre_approval_gate` evidence, and its zero-suite CI state is accepted as credibly green, so the PR is at the final approval boundary."
|
|
1522
1785
|
: "The current head has both a clean settled review cycle and clean `pre_approval_gate` evidence, so the PR is at the final approval boundary."),
|
|
@@ -1549,7 +1812,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1549
1812
|
reason: roundCapReached
|
|
1550
1813
|
? `The Copilot round limit is exhausted (${copilotReviewRoundCount}/${maxCopilotRounds}), and the current head has zero unresolved threads with ${describeAcceptedCiState(ciStatus, preApprovalRequireCi)}, so \`pre_approval_gate\` fallback is now the next legal boundary.`
|
|
1551
1814
|
: (postConvergenceReviewSuppressed && !sameHeadCleanConverged
|
|
1552
|
-
? "
|
|
1815
|
+
? "The current head carries the prior converged Copilot review: no request is outstanding, no review thread is unresolved, and the delta since Copilot's last submitted review is provably docs-only or integrate-only, so `pre_approval_gate` is now the next legal boundary."
|
|
1553
1816
|
: (ciStatus === "crediblyGreen"
|
|
1554
1817
|
? "The current head has a clean settled post-draft review cycle, and its zero-suite CI state is accepted as credibly green, so `pre_approval_gate` is now the next legal boundary."
|
|
1555
1818
|
: "The current head has a clean settled post-draft review cycle, so `pre_approval_gate` is now the next legal boundary.")),
|
|
@@ -1585,7 +1848,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1585
1848
|
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
1586
1849
|
loopDisposition: DISPOSITION.BLOCKED,
|
|
1587
1850
|
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
1588
|
-
draftGateAlreadySatisfied
|
|
1851
|
+
draftGateAlreadySatisfied,
|
|
1589
1852
|
draftGate,
|
|
1590
1853
|
preApprovalGate,
|
|
1591
1854
|
allowedNextActions,
|
|
@@ -1609,7 +1872,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1609
1872
|
lifecycleState: STATE.WAITING_FOR_CI,
|
|
1610
1873
|
loopDisposition: DISPOSITION.PENDING,
|
|
1611
1874
|
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
1612
|
-
draftGateAlreadySatisfied
|
|
1875
|
+
draftGateAlreadySatisfied,
|
|
1613
1876
|
draftGate,
|
|
1614
1877
|
preApprovalGate,
|
|
1615
1878
|
allowedNextActions,
|
|
@@ -1627,7 +1890,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1627
1890
|
return buildTitleMarkerBlockedResult({
|
|
1628
1891
|
input,
|
|
1629
1892
|
currentHeadSha,
|
|
1630
|
-
draftGateAlreadySatisfied
|
|
1893
|
+
draftGateAlreadySatisfied,
|
|
1631
1894
|
draftGate,
|
|
1632
1895
|
preApprovalGate,
|
|
1633
1896
|
mergeStateStatus,
|
|
@@ -1638,11 +1901,12 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1638
1901
|
}
|
|
1639
1902
|
// Mirror LOW_SIGNAL_CONVERGED: a clean current head with no clean
|
|
1640
1903
|
// draft_gate evidence must reconcile the draft gate rather than jump to
|
|
1641
|
-
// final approval. This
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1644
|
-
//
|
|
1645
|
-
//
|
|
1904
|
+
// final approval. This is the SAME rule applyDraftGateEvidenceGuard
|
|
1905
|
+
// enforces generically for every DRAFT_GATE_EVIDENCE_GUARDED_BOUNDARIES
|
|
1906
|
+
// boundary on a non-draft PR (draftGate.cleanEvidenceExists false →
|
|
1907
|
+
// reconcile_draft_gate); this inline check covers the same outcome for
|
|
1908
|
+
// FINAL_APPROVAL_READY specifically, before that wrapper-level guard
|
|
1909
|
+
// runs. Without this guard the final-approval-without-draft-gate
|
|
1646
1910
|
// branch is dead through the real script and asserts behavior it never
|
|
1647
1911
|
// produces.
|
|
1648
1912
|
if (!draftGate.cleanEvidenceExists) {
|
|
@@ -1675,13 +1939,13 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1675
1939
|
lifecycleState: effectiveLifecycleState,
|
|
1676
1940
|
loopDisposition: loopDisposition ?? DISPOSITION.CLEAN_CONVERGED,
|
|
1677
1941
|
gateBoundary: PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
1678
|
-
draftGateAlreadySatisfied
|
|
1942
|
+
draftGateAlreadySatisfied,
|
|
1679
1943
|
draftGate,
|
|
1680
1944
|
preApprovalGate,
|
|
1681
1945
|
allowedNextActions,
|
|
1682
1946
|
forbiddenActions,
|
|
1683
1947
|
nextAction: PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
1684
|
-
reason: `Round-cap clean fallback
|
|
1948
|
+
reason: `Round-cap clean fallback (${copilotReviewRoundCount}/${maxCopilotRounds} rounds, zero unresolved threads, ${describeAcceptedCiState(ciStatus, preApprovalRequireCi)}) with clean draft_gate evidence; the current head has clean \`pre_approval_gate\` evidence, so the PR is at the final approval boundary.`,
|
|
1685
1949
|
mergeStateStatus,
|
|
1686
1950
|
conflictFiles,
|
|
1687
1951
|
refinementArtifact,
|
|
@@ -1702,7 +1966,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1702
1966
|
lifecycleState: effectiveLifecycleState,
|
|
1703
1967
|
loopDisposition: loopDisposition ?? DISPOSITION.CLEAN_CONVERGED,
|
|
1704
1968
|
gateBoundary: PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
1705
|
-
draftGateAlreadySatisfied
|
|
1969
|
+
draftGateAlreadySatisfied,
|
|
1706
1970
|
draftGate,
|
|
1707
1971
|
preApprovalGate,
|
|
1708
1972
|
allowedNextActions,
|
|
@@ -1745,7 +2009,15 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1745
2009
|
// human-read reason/evidence text for the latter case would be a false CI
|
|
1746
2010
|
// claim, so describe the actual grant basis instead.
|
|
1747
2011
|
const ciClause = ciStatus === "success" ? "green CI" : "CI not required by config";
|
|
1748
|
-
|
|
2012
|
+
// No current-head Copilot review at the cap IS the round-cap clean
|
|
2013
|
+
// fallback, so the convergence evaluator's absent-review refusal must not
|
|
2014
|
+
// block this grant; a current-head review with findings still does. An
|
|
2015
|
+
// unknown head never opens the absent-review branch. Nor does an
|
|
2016
|
+
// earlier-head body-only finding without a disposition record: the caller
|
|
2017
|
+
// must state it absent (fail closed when the fact is missing).
|
|
2018
|
+
const absentReviewFallback = currentHeadSha !== null && input.copilotReviewOnCurrentHead === false
|
|
2019
|
+
&& input.copilotPriorHeadBodyFeedbackUnresolved === false;
|
|
2020
|
+
if (unresolvedThreadCount === 0 && ciConfirmedGreen && (copilotConvergenceOk || absentReviewFallback)) {
|
|
1749
2021
|
if (preApprovalGate.currentHeadClean) {
|
|
1750
2022
|
// Inline title-marker check, mirroring ROUND_CAP_CLEAN_FALLBACK: the
|
|
1751
2023
|
// outer post-pass guards FINAL_APPROVAL_READY and
|
|
@@ -1757,7 +2029,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1757
2029
|
return buildTitleMarkerBlockedResult({
|
|
1758
2030
|
input,
|
|
1759
2031
|
currentHeadSha,
|
|
1760
|
-
draftGateAlreadySatisfied
|
|
2032
|
+
draftGateAlreadySatisfied,
|
|
1761
2033
|
draftGate,
|
|
1762
2034
|
preApprovalGate,
|
|
1763
2035
|
mergeStateStatus,
|
|
@@ -1797,13 +2069,13 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1797
2069
|
lifecycleState: effectiveLifecycleState,
|
|
1798
2070
|
loopDisposition: loopDisposition ?? DISPOSITION.CLEAN_CONVERGED,
|
|
1799
2071
|
gateBoundary: PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
1800
|
-
draftGateAlreadySatisfied
|
|
2072
|
+
draftGateAlreadySatisfied,
|
|
1801
2073
|
draftGate,
|
|
1802
2074
|
preApprovalGate,
|
|
1803
2075
|
allowedNextActions,
|
|
1804
2076
|
forbiddenActions,
|
|
1805
2077
|
nextAction: PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
1806
|
-
reason: `Round-cap exhaustion fallback
|
|
2078
|
+
reason: `Round-cap exhaustion fallback (${copilotReviewRoundCount}/${maxCopilotRounds} rounds, zero unresolved threads, ${ciClause}) with clean draft_gate evidence; the current head has clean \`pre_approval_gate\` evidence, so the PR is at the final approval boundary.`,
|
|
1807
2079
|
mergeStateStatus,
|
|
1808
2080
|
conflictFiles,
|
|
1809
2081
|
refinementArtifact,
|
|
@@ -1825,7 +2097,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1825
2097
|
lifecycleState: effectiveLifecycleState,
|
|
1826
2098
|
loopDisposition: loopDisposition ?? DISPOSITION.CLEAN_CONVERGED,
|
|
1827
2099
|
gateBoundary: PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
1828
|
-
draftGateAlreadySatisfied
|
|
2100
|
+
draftGateAlreadySatisfied,
|
|
1829
2101
|
draftGate,
|
|
1830
2102
|
preApprovalGate,
|
|
1831
2103
|
allowedNextActions,
|
|
@@ -1852,7 +2124,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1852
2124
|
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
1853
2125
|
loopDisposition: DISPOSITION.BLOCKED,
|
|
1854
2126
|
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
1855
|
-
draftGateAlreadySatisfied
|
|
2127
|
+
draftGateAlreadySatisfied,
|
|
1856
2128
|
draftGate,
|
|
1857
2129
|
preApprovalGate,
|
|
1858
2130
|
allowedNextActions,
|
|
@@ -1876,7 +2148,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1876
2148
|
lifecycleState: STATE.WAITING_FOR_CI,
|
|
1877
2149
|
loopDisposition: DISPOSITION.PENDING,
|
|
1878
2150
|
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
1879
|
-
draftGateAlreadySatisfied
|
|
2151
|
+
draftGateAlreadySatisfied,
|
|
1880
2152
|
draftGate,
|
|
1881
2153
|
preApprovalGate,
|
|
1882
2154
|
allowedNextActions,
|
|
@@ -1894,7 +2166,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1894
2166
|
return buildTitleMarkerBlockedResult({
|
|
1895
2167
|
input,
|
|
1896
2168
|
currentHeadSha,
|
|
1897
|
-
draftGateAlreadySatisfied
|
|
2169
|
+
draftGateAlreadySatisfied,
|
|
1898
2170
|
draftGate,
|
|
1899
2171
|
preApprovalGate,
|
|
1900
2172
|
mergeStateStatus,
|
|
@@ -1932,7 +2204,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1932
2204
|
lifecycleState: effectiveLifecycleState,
|
|
1933
2205
|
loopDisposition: DISPOSITION.DONE,
|
|
1934
2206
|
gateBoundary: PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
1935
|
-
draftGateAlreadySatisfied
|
|
2207
|
+
draftGateAlreadySatisfied,
|
|
1936
2208
|
draftGate,
|
|
1937
2209
|
preApprovalGate,
|
|
1938
2210
|
allowedNextActions,
|