@dev-loops/core 0.1.0
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/bin/capture-deep-persona-signals.mjs +143 -0
- package/bin/ensure-phase-files.mjs +7 -0
- package/bin/log-bash-exit-1.mjs +7 -0
- package/bin/parse-review-threads.mjs +7 -0
- package/package.json +78 -0
- package/src/analysis/change-classifier.mjs +146 -0
- package/src/analysis/diff-analyzer.mjs +285 -0
- package/src/bash-exit-one.mjs +130 -0
- package/src/cli/helpers.mjs +22 -0
- package/src/cli/primitives.mjs +70 -0
- package/src/cli/retry-wrapper.mjs +169 -0
- package/src/cli/subcommand-runner.mjs +246 -0
- package/src/config/config.mjs +965 -0
- package/src/debt/cluster.mjs +240 -0
- package/src/debt/debt-finding.mjs +68 -0
- package/src/debt/debt-signal.mjs +46 -0
- package/src/debt/deep-persona-signals.mjs +266 -0
- package/src/debt/remediation-to-issue.mjs +121 -0
- package/src/debt/score.mjs +127 -0
- package/src/debt/shape.mjs +214 -0
- package/src/github/copilot-helpers.mjs +343 -0
- package/src/github/repo-slug.mjs +105 -0
- package/src/github/review-threads.mjs +343 -0
- package/src/harness/adapter.mjs +57 -0
- package/src/harness/index.mjs +3 -0
- package/src/harness/noop-adapter.mjs +22 -0
- package/src/harness/pi-adapter.mjs +47 -0
- package/src/loop/async-start-contract.mjs +170 -0
- package/src/loop/conductor-routing.mjs +817 -0
- package/src/loop/copilot-ci-status.mjs +255 -0
- package/src/loop/copilot-loop-iterations.mjs +161 -0
- package/src/loop/copilot-loop-state.mjs +510 -0
- package/src/loop/handoff-envelope.mjs +800 -0
- package/src/loop/issue-refinement-artifact.mjs +268 -0
- package/src/loop/lifecycle-state.mjs +342 -0
- package/src/loop/phase-files.mjs +187 -0
- package/src/loop/policy-constants.mjs +17 -0
- package/src/loop/pr-gate-coordination.mjs +1278 -0
- package/src/loop/public-dev-loop-routing-contract.mjs +277 -0
- package/src/loop/public-dev-loop-routing.mjs +1746 -0
- package/src/loop/queue-board-ordering.mjs +38 -0
- package/src/loop/queue-board-sync.mjs +223 -0
- package/src/loop/queue-driver.mjs +164 -0
- package/src/loop/queue-parallel.mjs +190 -0
- package/src/loop/queue-state.mjs +230 -0
- package/src/loop/retrospective-checkpoint.mjs +178 -0
- package/src/loop/reviewer-loop-state.mjs +456 -0
- package/src/loop/run-inspection.mjs +604 -0
- package/src/loop/steering.mjs +793 -0
- package/src/loop/timeout-policy.mjs +73 -0
- package/src/loop/tracker-first-loop-state.mjs +87 -0
- package/src/loop/tracker-pr-state.mjs +301 -0
- package/src/loop/worktree-guard.mjs +141 -0
- package/src/refinement/ac-dod-matrix.mjs +95 -0
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
import { DISPOSITION, STATE } from "./copilot-loop-state.mjs";
|
|
2
|
+
|
|
3
|
+
export const PR_CHECKPOINT = Object.freeze({
|
|
4
|
+
DRAFT_REVIEW: "draft_review",
|
|
5
|
+
POST_DRAFT_EXTERNAL_REVIEW: "post_draft_external_review",
|
|
6
|
+
FEEDBACK_RESOLUTION: "feedback_resolution",
|
|
7
|
+
CONFLICT_RESOLUTION: "conflict_resolution",
|
|
8
|
+
PRE_APPROVAL_GATE_WINDOW: "pre_approval_gate_window",
|
|
9
|
+
FINAL_APPROVAL_READY: "final_approval_ready",
|
|
10
|
+
PRE_APPROVAL_GATE_NEEDED: "pre_approval_gate_needed",
|
|
11
|
+
DRAFT_GATE_NEEDED: "draft_gate_needed",
|
|
12
|
+
BLOCKED: "blocked",
|
|
13
|
+
DONE: "done",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Refinement-artifact gate check (issue #532).
|
|
19
|
+
*
|
|
20
|
+
* The draft gate must verify the linked issue has an explicit refinement
|
|
21
|
+
* artifact (Acceptance criteria / DoD / linked refinement doc) before it
|
|
22
|
+
* can post a clean verdict. When the artifact is missing the draft gate
|
|
23
|
+
* must post verdict=blocked with the missing_refinement_artifact finding
|
|
24
|
+
* and the PR cannot leave draft.
|
|
25
|
+
*/
|
|
26
|
+
export const REFINEMENT_ARTIFACT_STATUS = Object.freeze({
|
|
27
|
+
MISSING: "missing",
|
|
28
|
+
PRESENT: "present",
|
|
29
|
+
UNKNOWN: "unknown",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const REFINEMENT_ARTIFACT_FINDING = "missing_refinement_artifact";
|
|
33
|
+
|
|
34
|
+
export const PR_CHECKPOINT_ACTION = Object.freeze({
|
|
35
|
+
RUN_DRAFT_GATE: "run_draft_gate",
|
|
36
|
+
MARK_READY_FOR_REVIEW: "mark_ready_for_review",
|
|
37
|
+
REQUEST_COPILOT_REVIEW: "request_copilot_review",
|
|
38
|
+
WAIT_FOR_COPILOT_REVIEW: "wait_for_copilot_review",
|
|
39
|
+
WAIT_FOR_CI: "wait_for_ci",
|
|
40
|
+
ADDRESS_REVIEW_FEEDBACK: "address_review_feedback",
|
|
41
|
+
REPLY_RESOLVE_REVIEW_THREADS: "reply_resolve_review_threads",
|
|
42
|
+
REREQUEST_COPILOT_REVIEW: "rerequest_copilot_review",
|
|
43
|
+
RESOLVE_MERGE_CONFLICTS: "resolve_merge_conflicts",
|
|
44
|
+
RUN_PRE_APPROVAL_GATE: "run_pre_approval_gate",
|
|
45
|
+
AWAIT_FINAL_HUMAN_APPROVAL: "await_final_human_approval",
|
|
46
|
+
DECLARE_MERGE_READY: "declare_merge_ready",
|
|
47
|
+
RECONCILE_DRAFT_GATE: "reconcile_draft_gate",
|
|
48
|
+
REPORT_BLOCKED: "report_blocked",
|
|
49
|
+
REPORT_DONE: "report_done",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function normalizeGateComment(summary = null) {
|
|
53
|
+
if (!summary || typeof summary !== "object") {
|
|
54
|
+
return {
|
|
55
|
+
visible: false,
|
|
56
|
+
headSha: null,
|
|
57
|
+
verdict: null,
|
|
58
|
+
findingsSummary: null,
|
|
59
|
+
nextAction: null,
|
|
60
|
+
contractComplete: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
visible: summary.visible === true,
|
|
66
|
+
headSha: typeof summary.headSha === "string" && summary.headSha.trim().length > 0 ? summary.headSha.trim() : null,
|
|
67
|
+
verdict: typeof summary.verdict === "string" && summary.verdict.trim().length > 0 ? summary.verdict.trim().toLowerCase() : null,
|
|
68
|
+
findingsSummary: typeof summary.findingsSummary === "string" && summary.findingsSummary.trim().length > 0
|
|
69
|
+
? summary.findingsSummary.trim()
|
|
70
|
+
: null,
|
|
71
|
+
nextAction: typeof summary.nextAction === "string" && summary.nextAction.trim().length > 0
|
|
72
|
+
? summary.nextAction.trim()
|
|
73
|
+
: null,
|
|
74
|
+
contractComplete: summary.contractComplete === true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toGateStatus(comment, marker, currentHeadSha) {
|
|
79
|
+
const normalizedComment = normalizeGateComment(comment);
|
|
80
|
+
const normalizedMarker = normalizeGateComment(marker);
|
|
81
|
+
const markerHeadMatches = normalizedMarker.headSha !== null
|
|
82
|
+
&& typeof currentHeadSha === "string"
|
|
83
|
+
&& currentHeadSha.startsWith(normalizedMarker.headSha);
|
|
84
|
+
const anyVisible = normalizedComment.visible || normalizedMarker.visible;
|
|
85
|
+
|
|
86
|
+
const cleanEvidenceExists = normalizedComment.visible && normalizedComment.verdict === "clean" && normalizedComment.headSha !== null;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
visible: normalizedComment.visible,
|
|
90
|
+
markerVisible: normalizedMarker.visible,
|
|
91
|
+
anyVisible,
|
|
92
|
+
currentHead: normalizedMarker.visible && markerHeadMatches,
|
|
93
|
+
headSha: normalizedComment.headSha ?? normalizedMarker.headSha,
|
|
94
|
+
verdict: normalizedComment.verdict ?? normalizedMarker.verdict,
|
|
95
|
+
findingsSummary: normalizedComment.findingsSummary ?? normalizedMarker.findingsSummary,
|
|
96
|
+
nextAction: normalizedComment.nextAction ?? normalizedMarker.nextAction,
|
|
97
|
+
contractComplete: normalizedMarker.visible && markerHeadMatches && normalizedMarker.contractComplete,
|
|
98
|
+
currentHeadClean: normalizedMarker.visible && markerHeadMatches && normalizedMarker.verdict === "clean" && normalizedMarker.contractComplete,
|
|
99
|
+
cleanEvidenceExists,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pushUnique(values, additions) {
|
|
104
|
+
for (const value of additions) {
|
|
105
|
+
if (typeof value === "string" && value.length > 0 && !values.includes(value)) {
|
|
106
|
+
values.push(value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const BLOCKED_MERGE_STATE_STATUSES = new Set(["DIRTY", "CONFLICTING", "BEHIND"]);
|
|
112
|
+
|
|
113
|
+
function normalizeMergeStateStatus(value) {
|
|
114
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return value.trim().toUpperCase();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeConflictFiles(value) {
|
|
122
|
+
if (!Array.isArray(value)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const normalized = [];
|
|
127
|
+
for (const entry of value) {
|
|
128
|
+
if (typeof entry !== "string") {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (entry.trim().length > 0 && !normalized.includes(entry)) {
|
|
133
|
+
normalized.push(entry);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return normalized;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hasBlockedMergeStatus(mergeStateStatus) {
|
|
141
|
+
return mergeStateStatus !== null && BLOCKED_MERGE_STATE_STATUSES.has(mergeStateStatus);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatBlockedMergeReason(mergeStateStatus, conflictFiles) {
|
|
145
|
+
if (mergeStateStatus === "BEHIND") {
|
|
146
|
+
let reason = "Branch must be updated from base before entering any gate.";
|
|
147
|
+
reason += ` GitHub mergeStateStatus: ${mergeStateStatus}.`;
|
|
148
|
+
return reason;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let reason = "The current branch conflicts with the base branch, so resolve the conflict locally on the PR branch, rerun validation, rerun gate detection, and only then resume the normal gate path.";
|
|
152
|
+
|
|
153
|
+
if (mergeStateStatus !== null) {
|
|
154
|
+
reason += ` GitHub mergeStateStatus: ${mergeStateStatus}.`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (conflictFiles.length > 0) {
|
|
158
|
+
reason += ` Conflicting files: ${conflictFiles.join(", ")}.`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return reason;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeCiStatus(value) {
|
|
165
|
+
if (typeof value !== "string") {
|
|
166
|
+
return "none";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const trimmed = value.trim();
|
|
170
|
+
if (trimmed.length === 0) {
|
|
171
|
+
return "none";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const lower = trimmed.toLowerCase();
|
|
175
|
+
if (lower === "success" || lower === "failure" || lower === "pending" || lower === "none") {
|
|
176
|
+
return lower;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (lower === "crediblygreen") {
|
|
180
|
+
return "crediblyGreen";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return "none";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeNonNegativeInteger(value) {
|
|
187
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return Math.floor(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizePositiveInteger(value) {
|
|
195
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return Math.floor(value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeRefinementArtifactStatus(value) {
|
|
203
|
+
if (value === REFINEMENT_ARTIFACT_STATUS.MISSING || value === REFINEMENT_ARTIFACT_STATUS.PRESENT) {
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
return REFINEMENT_ARTIFACT_STATUS.UNKNOWN;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatRefinementBlockedReason(linkedIssue, status) {
|
|
210
|
+
if (linkedIssue !== null && Number.isInteger(linkedIssue)) {
|
|
211
|
+
return `Linked issue #${linkedIssue} has no refinement artifact (Acceptance criteria / DoD / linked refinement doc). Run refinement first, add ACs/DoD to the issue, then re-open the draft PR. finding=${REFINEMENT_ARTIFACT_FINDING}`;
|
|
212
|
+
}
|
|
213
|
+
return `The draft gate cannot complete: the linked issue has no detectable refinement artifact (Acceptance criteria / DoD / linked refinement doc). finding=${REFINEMENT_ARTIFACT_FINDING}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildRoundExhaustionGateEvidenceNote({ copilotReviewRoundCount, maxCopilotRounds }) {
|
|
217
|
+
return `Copilot review rounds exhausted (${copilotReviewRoundCount}/${maxCopilotRounds}); current head has zero unresolved threads and green or credibly green CI, so pre_approval_gate fallback is allowed without another Copilot re-request.`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function evaluateRetrospectiveMergeApproval(checkpoint) {
|
|
221
|
+
if (!checkpoint || typeof checkpoint !== "object") {
|
|
222
|
+
return { approved: false, reason: "No retrospective checkpoint was found." };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const state = typeof checkpoint.state === "string" ? checkpoint.state.trim().toLowerCase() : "";
|
|
226
|
+
if (state !== "complete") {
|
|
227
|
+
return { approved: false, reason: `Retrospective is not complete (state: ${state || "missing"}).` };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Read merge approval from behavioralReview (existing format) or top-level (future flat format).
|
|
231
|
+
const br = checkpoint.behavioralReview && typeof checkpoint.behavioralReview === "object"
|
|
232
|
+
? checkpoint.behavioralReview
|
|
233
|
+
: null;
|
|
234
|
+
const mergeApproved = br !== null ? br.mergeApproved : checkpoint.mergeApproved;
|
|
235
|
+
if (mergeApproved !== true) {
|
|
236
|
+
return { approved: false, reason: "Retrospective does not explicitly approve merge (`mergeApproved: true` is required)." };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// followedWorkingAgreement: required boolean (existing checkpoint uses behavioralReview.followedWorkingAgreement).
|
|
240
|
+
const followedWorkingAgreement = br !== null
|
|
241
|
+
? br.followedWorkingAgreement
|
|
242
|
+
: checkpoint.followedWorkingAgreement;
|
|
243
|
+
if (typeof followedWorkingAgreement !== "boolean") {
|
|
244
|
+
return { approved: false, reason: "Retrospective is missing `followedWorkingAgreement` (true/false)." };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// gateQuality: require gateQualityAcceptable=true AND non-empty notes (behavioralReview)
|
|
248
|
+
// or explicit gateQuality string (flat format). Avoid empty-notes bypass.
|
|
249
|
+
const gateQualityAcceptable = br !== null
|
|
250
|
+
? br.gateQualityAcceptable
|
|
251
|
+
: checkpoint.gateQualityAcceptable;
|
|
252
|
+
if (typeof gateQualityAcceptable !== "boolean" || gateQualityAcceptable !== true) {
|
|
253
|
+
return { approved: false, reason: `Retrospective gate quality is not explicitly acceptable (gateQualityAcceptable: ${String(gateQualityAcceptable)}).` };
|
|
254
|
+
}
|
|
255
|
+
const gateQuality = typeof checkpoint.gateQuality === "string" && checkpoint.gateQuality.trim().length > 0
|
|
256
|
+
? checkpoint.gateQuality
|
|
257
|
+
: null;
|
|
258
|
+
if (!gateQuality) {
|
|
259
|
+
return { approved: false, reason: "Retrospective is missing `gateQuality` details; provide a notes field with gate-quality assessment or an explicit gateQuality string." };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// unexpectedFindings: derive from behavioralReview.drifts if flat field absent. Empty array is valid (no findings).
|
|
263
|
+
const unexpectedFindings = typeof checkpoint.unexpectedFindings === "string" && checkpoint.unexpectedFindings.trim().length > 0
|
|
264
|
+
? checkpoint.unexpectedFindings
|
|
265
|
+
: (br !== null && Array.isArray(br.drifts)
|
|
266
|
+
? (br.drifts.length > 0 ? br.drifts.join("; ") : "none")
|
|
267
|
+
: null);
|
|
268
|
+
if (!unexpectedFindings) {
|
|
269
|
+
return { approved: false, reason: "Retrospective is missing `unexpectedFindings` details." };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// mergeRecommendation: require explicit mergeRecommendation field (string).
|
|
273
|
+
const mergeRecommendation = typeof checkpoint.mergeRecommendation === "string" && checkpoint.mergeRecommendation.trim().length > 0
|
|
274
|
+
? checkpoint.mergeRecommendation
|
|
275
|
+
: null;
|
|
276
|
+
if (!mergeRecommendation) {
|
|
277
|
+
return { approved: false, reason: "Retrospective is missing explicit `mergeRecommendation`." };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { approved: true, reason: null };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildRetrospectiveGatePendingResult({
|
|
284
|
+
input,
|
|
285
|
+
currentHeadSha,
|
|
286
|
+
draftGateAlreadySatisfied,
|
|
287
|
+
draftGate,
|
|
288
|
+
preApprovalGate,
|
|
289
|
+
mergeStateStatus,
|
|
290
|
+
conflictFiles,
|
|
291
|
+
reason,
|
|
292
|
+
refinementArtifact = null,
|
|
293
|
+
}) {
|
|
294
|
+
const allowedNextActions = [];
|
|
295
|
+
const forbiddenActions = [];
|
|
296
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_BLOCKED]);
|
|
297
|
+
pushUnique(forbiddenActions, [
|
|
298
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
299
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
300
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
301
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
302
|
+
PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
303
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
return buildResult({
|
|
307
|
+
repo: input.repo ?? null,
|
|
308
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
309
|
+
currentHeadSha,
|
|
310
|
+
lifecycleState: "retrospective_gate_pending",
|
|
311
|
+
loopDisposition: DISPOSITION.BLOCKED,
|
|
312
|
+
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
313
|
+
draftGateAlreadySatisfied,
|
|
314
|
+
draftGate,
|
|
315
|
+
preApprovalGate,
|
|
316
|
+
allowedNextActions,
|
|
317
|
+
forbiddenActions,
|
|
318
|
+
nextAction: PR_CHECKPOINT_ACTION.REPORT_BLOCKED,
|
|
319
|
+
reason,
|
|
320
|
+
mergeStateStatus,
|
|
321
|
+
conflictFiles,
|
|
322
|
+
refinementArtifact,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
function buildDraftGateNeededForMergeResult({
|
|
328
|
+
input,
|
|
329
|
+
currentHeadSha,
|
|
330
|
+
draftGate,
|
|
331
|
+
preApprovalGate,
|
|
332
|
+
mergeStateStatus,
|
|
333
|
+
conflictFiles,
|
|
334
|
+
underlyingReason = null,
|
|
335
|
+
refinementArtifact = null,
|
|
336
|
+
effectiveLifecycleState = null,
|
|
337
|
+
}) {
|
|
338
|
+
const allowedNextActions = [];
|
|
339
|
+
const forbiddenActions = [];
|
|
340
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.RECONCILE_DRAFT_GATE]);
|
|
341
|
+
pushUnique(forbiddenActions, [
|
|
342
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
343
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
344
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
345
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
346
|
+
PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
347
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
return buildResult({
|
|
351
|
+
repo: input.repo ?? null,
|
|
352
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
353
|
+
currentHeadSha,
|
|
354
|
+
lifecycleState: effectiveLifecycleState ?? STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
355
|
+
loopDisposition: DISPOSITION.ACTION_REQUIRED,
|
|
356
|
+
gateBoundary: PR_CHECKPOINT.DRAFT_GATE_NEEDED,
|
|
357
|
+
draftGateAlreadySatisfied: false,
|
|
358
|
+
draftGate,
|
|
359
|
+
preApprovalGate,
|
|
360
|
+
allowedNextActions,
|
|
361
|
+
forbiddenActions,
|
|
362
|
+
nextAction: PR_CHECKPOINT_ACTION.RECONCILE_DRAFT_GATE,
|
|
363
|
+
reason: `Clean draft_gate evidence is required before merge (no gate exemptions, #579).${draftGate?.anyVisible ? " 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." : " No visible clean draft_gate comment exists for this PR; run reconcile_draft_gate before proceeding."}${underlyingReason ? ` ${underlyingReason}` : ""}`,
|
|
364
|
+
mergeStateStatus,
|
|
365
|
+
conflictFiles,
|
|
366
|
+
refinementArtifact,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function buildResult({
|
|
370
|
+
draftGateAlreadySatisfied = false,
|
|
371
|
+
repo = null,
|
|
372
|
+
pr = null,
|
|
373
|
+
currentHeadSha = null,
|
|
374
|
+
lifecycleState,
|
|
375
|
+
loopDisposition,
|
|
376
|
+
gateBoundary,
|
|
377
|
+
draftGate,
|
|
378
|
+
preApprovalGate,
|
|
379
|
+
allowedNextActions,
|
|
380
|
+
forbiddenActions,
|
|
381
|
+
nextAction,
|
|
382
|
+
reason,
|
|
383
|
+
mergeStateStatus = null,
|
|
384
|
+
conflictFiles = [],
|
|
385
|
+
gateEvidenceNote = null,
|
|
386
|
+
refinementArtifact = null,
|
|
387
|
+
inputRefinementArtifact = null,
|
|
388
|
+
copilotReviewRoundCount = null,
|
|
389
|
+
}) {
|
|
390
|
+
const effectiveRefinementArtifact = refinementArtifact ?? inputRefinementArtifact ?? null;
|
|
391
|
+
return {
|
|
392
|
+
ok: true,
|
|
393
|
+
...(repo ? { repo } : {}),
|
|
394
|
+
...(pr !== null ? { pr } : {}),
|
|
395
|
+
currentHeadSha,
|
|
396
|
+
lifecycleState,
|
|
397
|
+
loopDisposition,
|
|
398
|
+
gateBoundary,
|
|
399
|
+
draftGate,
|
|
400
|
+
preApprovalGate,
|
|
401
|
+
allowedNextActions,
|
|
402
|
+
forbiddenActions,
|
|
403
|
+
nextAction,
|
|
404
|
+
reason,
|
|
405
|
+
mergeStateStatus,
|
|
406
|
+
conflictFiles,
|
|
407
|
+
draftGateAlreadySatisfied,
|
|
408
|
+
copilotReviewRoundCount,
|
|
409
|
+
gateEvidenceRequiredForMerge: true,
|
|
410
|
+
...(gateEvidenceNote ? { gateEvidenceNote } : {}),
|
|
411
|
+
...(effectiveRefinementArtifact ? { refinementArtifact: effectiveRefinementArtifact } : {}),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Guard: detect when Copilot has reviewed the PR without a formal
|
|
418
|
+
* review request and the gate boundary is pre_approval_gate territory.
|
|
419
|
+
* Returns true when the pre-approval gate should be blocked and the
|
|
420
|
+
* caller must run request-copilot-review.mjs first.
|
|
421
|
+
*
|
|
422
|
+
* Exception: round-cap clean fallback (rounds exhausted + clean converged)
|
|
423
|
+
* does not require a formal re-request (#613).
|
|
424
|
+
*
|
|
425
|
+
* @param {object} params
|
|
426
|
+
* @param {string} params.copilotReviewRequestStatus - "none"|"requested"|"already-requested"|"unavailable"|"failed"
|
|
427
|
+
* @param {boolean} [params.copilotReviewEverFormallyRequested=false] - whether Copilot was ever formally requested via GitHub review_requested mechanism
|
|
428
|
+
* @param {number} params.copilotReviewRoundCount
|
|
429
|
+
* @param {number|null} params.maxCopilotRounds
|
|
430
|
+
* @param {boolean} params.sameHeadCleanConverged
|
|
431
|
+
* @param {string} params.gateBoundary - current gate boundary
|
|
432
|
+
* @returns {boolean}
|
|
433
|
+
*/
|
|
434
|
+
export function shouldGuardCopilotReviewRequest({
|
|
435
|
+
copilotReviewRequestStatus,
|
|
436
|
+
copilotReviewRoundCount = 0,
|
|
437
|
+
copilotReviewEverFormallyRequested = false,
|
|
438
|
+
maxCopilotRounds = null,
|
|
439
|
+
sameHeadCleanConverged = false,
|
|
440
|
+
gateBoundary,
|
|
441
|
+
}) {
|
|
442
|
+
const gateBoundariesRequiringCopilotFormalRequest = new Set([
|
|
443
|
+
PR_CHECKPOINT.PRE_APPROVAL_GATE_NEEDED,
|
|
444
|
+
PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
445
|
+
PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
446
|
+
]);
|
|
447
|
+
if (!gateBoundariesRequiringCopilotFormalRequest.has(gateBoundary)) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
if (copilotReviewRequestStatus !== "none") {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
// Durable signal: if Copilot was ever formally requested as a reviewer,
|
|
454
|
+
// the current "none" status is from a fulfilled request (normal cycle),
|
|
455
|
+
// not from a missing request. Do not guard the happy path (#613, round 2).
|
|
456
|
+
if (copilotReviewEverFormallyRequested) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
// Round-cap clean fallback: exhausted rounds + clean converged
|
|
460
|
+
// does not require a formal re-request.
|
|
461
|
+
const roundCapReached = maxCopilotRounds !== null
|
|
462
|
+
&& typeof copilotReviewRoundCount === "number"
|
|
463
|
+
&& copilotReviewRoundCount >= maxCopilotRounds;
|
|
464
|
+
if (roundCapReached && sameHeadCleanConverged) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function evaluatePrGateCoordination(input = {}) {
|
|
471
|
+
const currentHeadSha = typeof input.currentHeadSha === "string" && input.currentHeadSha.trim().length > 0
|
|
472
|
+
? input.currentHeadSha.trim()
|
|
473
|
+
: null;
|
|
474
|
+
const lifecycleState = typeof input.lifecycleState === "string" ? input.lifecycleState.trim().toLowerCase() : "";
|
|
475
|
+
const loopDisposition = typeof input.loopDisposition === "string" ? input.loopDisposition.trim().toLowerCase() : null;
|
|
476
|
+
const prDraft = input.prDraft === true;
|
|
477
|
+
const prClosed = input.prClosed === true;
|
|
478
|
+
const prMerged = input.prMerged === true;
|
|
479
|
+
const sameHeadCleanConverged = input.sameHeadCleanConverged === true;
|
|
480
|
+
const reviewMode = typeof input.reviewMode === "string"
|
|
481
|
+
? input.reviewMode.trim().toLowerCase()
|
|
482
|
+
: null;
|
|
483
|
+
const mergeStateStatus = normalizeMergeStateStatus(input.mergeStateStatus);
|
|
484
|
+
const conflictFiles = normalizeConflictFiles(input.conflictFiles);
|
|
485
|
+
const ciStatus = normalizeCiStatus(input.ciStatus);
|
|
486
|
+
const draftGateRequireCi = input.draftGateRequireCi !== false;
|
|
487
|
+
const copilotReviewRoundCount = normalizeNonNegativeInteger(input.copilotReviewRoundCount);
|
|
488
|
+
const maxCopilotRounds = normalizePositiveInteger(input.maxCopilotRounds);
|
|
489
|
+
const roundCapReached = maxCopilotRounds !== null && copilotReviewRoundCount >= maxCopilotRounds;
|
|
490
|
+
const requireRetrospectiveGate = input.requireRetrospectiveGate === true;
|
|
491
|
+
const retrospectiveCheckpoint = input.retrospectiveCheckpoint;
|
|
492
|
+
const refinementArtifact = input.refinementArtifact && typeof input.refinementArtifact === "object"
|
|
493
|
+
? input.refinementArtifact
|
|
494
|
+
: null;
|
|
495
|
+
const refinementArtifactStatus = normalizeRefinementArtifactStatus(refinementArtifact?.status);
|
|
496
|
+
const refinementLinkedIssue = Number.isInteger(refinementArtifact?.linkedIssue) ? refinementArtifact.linkedIssue : null;
|
|
497
|
+
|
|
498
|
+
const effectiveLifecycleState = lifecycleState;
|
|
499
|
+
|
|
500
|
+
const draftGate = toGateStatus(input.draftGate, input.draftGateMarker, currentHeadSha);
|
|
501
|
+
const preApprovalGate = toGateStatus(input.preApprovalGate, input.preApprovalGateMarker, currentHeadSha);
|
|
502
|
+
const draftGateAlreadySatisfied = !prDraft && (draftGate?.cleanEvidenceExists ?? false);
|
|
503
|
+
|
|
504
|
+
const allowedNextActions = [];
|
|
505
|
+
const forbiddenActions = [];
|
|
506
|
+
|
|
507
|
+
if (prMerged || prClosed || effectiveLifecycleState === STATE.DONE) {
|
|
508
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_DONE]);
|
|
509
|
+
pushUnique(forbiddenActions, [
|
|
510
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
511
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
512
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
513
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
514
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
515
|
+
]);
|
|
516
|
+
return buildResult({
|
|
517
|
+
repo: input.repo ?? null,
|
|
518
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
519
|
+
currentHeadSha,
|
|
520
|
+
lifecycleState: effectiveLifecycleState,
|
|
521
|
+
loopDisposition: loopDisposition ?? DISPOSITION.DONE,
|
|
522
|
+
gateBoundary: PR_CHECKPOINT.DONE,
|
|
523
|
+
draftGateAlreadySatisfied,
|
|
524
|
+
draftGate,
|
|
525
|
+
preApprovalGate,
|
|
526
|
+
allowedNextActions,
|
|
527
|
+
forbiddenActions,
|
|
528
|
+
nextAction: PR_CHECKPOINT_ACTION.REPORT_DONE,
|
|
529
|
+
reason: "The pull request is already closed or merged, so no further gate entry is legal.",
|
|
530
|
+
mergeStateStatus,
|
|
531
|
+
conflictFiles,
|
|
532
|
+
refinementArtifact,
|
|
533
|
+
copilotReviewRoundCount,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (effectiveLifecycleState === STATE.BLOCKED_NEEDS_USER_DECISION || effectiveLifecycleState === STATE.REVIEW_REQUEST_UNAVAILABLE) {
|
|
538
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_BLOCKED]);
|
|
539
|
+
pushUnique(forbiddenActions, [
|
|
540
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
541
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
542
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
543
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
544
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
545
|
+
]);
|
|
546
|
+
return buildResult({
|
|
547
|
+
repo: input.repo ?? null,
|
|
548
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
549
|
+
currentHeadSha,
|
|
550
|
+
lifecycleState: effectiveLifecycleState,
|
|
551
|
+
loopDisposition: loopDisposition ?? DISPOSITION.BLOCKED,
|
|
552
|
+
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
553
|
+
draftGateAlreadySatisfied,
|
|
554
|
+
draftGate,
|
|
555
|
+
preApprovalGate,
|
|
556
|
+
allowedNextActions,
|
|
557
|
+
forbiddenActions,
|
|
558
|
+
nextAction: PR_CHECKPOINT_ACTION.REPORT_BLOCKED,
|
|
559
|
+
reason: "The PR is in a blocked lifecycle state, so gate progression must stop for a user decision.",
|
|
560
|
+
mergeStateStatus,
|
|
561
|
+
conflictFiles,
|
|
562
|
+
refinementArtifact,
|
|
563
|
+
copilotReviewRoundCount,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (hasBlockedMergeStatus(mergeStateStatus) || conflictFiles.length > 0) {
|
|
568
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.RESOLVE_MERGE_CONFLICTS]);
|
|
569
|
+
pushUnique(forbiddenActions, [
|
|
570
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
571
|
+
PR_CHECKPOINT_ACTION.RECONCILE_DRAFT_GATE,
|
|
572
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
573
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
574
|
+
PR_CHECKPOINT_ACTION.WAIT_FOR_COPILOT_REVIEW,
|
|
575
|
+
PR_CHECKPOINT_ACTION.WAIT_FOR_CI,
|
|
576
|
+
PR_CHECKPOINT_ACTION.ADDRESS_REVIEW_FEEDBACK,
|
|
577
|
+
PR_CHECKPOINT_ACTION.REPLY_RESOLVE_REVIEW_THREADS,
|
|
578
|
+
PR_CHECKPOINT_ACTION.REREQUEST_COPILOT_REVIEW,
|
|
579
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
580
|
+
PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
581
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
582
|
+
]);
|
|
583
|
+
return buildResult({
|
|
584
|
+
repo: input.repo ?? null,
|
|
585
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
586
|
+
currentHeadSha,
|
|
587
|
+
lifecycleState: effectiveLifecycleState,
|
|
588
|
+
loopDisposition: DISPOSITION.ACTION_REQUIRED,
|
|
589
|
+
gateBoundary: PR_CHECKPOINT.CONFLICT_RESOLUTION,
|
|
590
|
+
draftGateAlreadySatisfied,
|
|
591
|
+
draftGate,
|
|
592
|
+
preApprovalGate,
|
|
593
|
+
allowedNextActions,
|
|
594
|
+
forbiddenActions,
|
|
595
|
+
nextAction: PR_CHECKPOINT_ACTION.RESOLVE_MERGE_CONFLICTS,
|
|
596
|
+
reason: formatBlockedMergeReason(mergeStateStatus, conflictFiles),
|
|
597
|
+
mergeStateStatus,
|
|
598
|
+
conflictFiles,
|
|
599
|
+
refinementArtifact,
|
|
600
|
+
copilotReviewRoundCount,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (prDraft || effectiveLifecycleState === STATE.PR_DRAFT) {
|
|
605
|
+
if (refinementArtifactStatus === REFINEMENT_ARTIFACT_STATUS.MISSING) {
|
|
606
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_BLOCKED]);
|
|
607
|
+
pushUnique(forbiddenActions, [
|
|
608
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
609
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
610
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
611
|
+
PR_CHECKPOINT_ACTION.WAIT_FOR_COPILOT_REVIEW,
|
|
612
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
613
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
614
|
+
]);
|
|
615
|
+
return buildResult({
|
|
616
|
+
repo: input.repo ?? null,
|
|
617
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
618
|
+
currentHeadSha,
|
|
619
|
+
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
620
|
+
loopDisposition: DISPOSITION.BLOCKED,
|
|
621
|
+
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
622
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
623
|
+
draftGate,
|
|
624
|
+
preApprovalGate,
|
|
625
|
+
allowedNextActions,
|
|
626
|
+
forbiddenActions,
|
|
627
|
+
nextAction: PR_CHECKPOINT_ACTION.REPORT_BLOCKED,
|
|
628
|
+
reason: formatRefinementBlockedReason(refinementLinkedIssue, refinementArtifactStatus),
|
|
629
|
+
mergeStateStatus,
|
|
630
|
+
conflictFiles,
|
|
631
|
+
refinementArtifact,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
const draftReviewForbidden = [
|
|
635
|
+
...(draftGate.currentHeadClean ? [] : [PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW]),
|
|
636
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
637
|
+
PR_CHECKPOINT_ACTION.WAIT_FOR_COPILOT_REVIEW,
|
|
638
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
639
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
640
|
+
];
|
|
641
|
+
|
|
642
|
+
if (!draftGate.currentHeadClean && draftGateRequireCi) {
|
|
643
|
+
if (ciStatus === "failure") {
|
|
644
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_BLOCKED]);
|
|
645
|
+
pushUnique(forbiddenActions, [
|
|
646
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
647
|
+
...draftReviewForbidden,
|
|
648
|
+
]);
|
|
649
|
+
return buildResult({
|
|
650
|
+
repo: input.repo ?? null,
|
|
651
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
652
|
+
currentHeadSha,
|
|
653
|
+
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
654
|
+
loopDisposition: DISPOSITION.BLOCKED,
|
|
655
|
+
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
656
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
657
|
+
draftGate,
|
|
658
|
+
preApprovalGate,
|
|
659
|
+
allowedNextActions,
|
|
660
|
+
forbiddenActions,
|
|
661
|
+
nextAction: PR_CHECKPOINT_ACTION.REPORT_BLOCKED,
|
|
662
|
+
reason: "The PR is still draft, and this repo requires green current-head CI before entering `draft_gate`. The current head is failing CI, so fix the checks before retrying the draft gate.",
|
|
663
|
+
mergeStateStatus,
|
|
664
|
+
conflictFiles,
|
|
665
|
+
refinementArtifact,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (ciStatus !== "success") {
|
|
670
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.WAIT_FOR_CI]);
|
|
671
|
+
pushUnique(forbiddenActions, [
|
|
672
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
673
|
+
...draftReviewForbidden,
|
|
674
|
+
]);
|
|
675
|
+
return buildResult({
|
|
676
|
+
repo: input.repo ?? null,
|
|
677
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
678
|
+
currentHeadSha,
|
|
679
|
+
lifecycleState: STATE.WAITING_FOR_CI,
|
|
680
|
+
loopDisposition: DISPOSITION.PENDING,
|
|
681
|
+
gateBoundary: PR_CHECKPOINT.DRAFT_REVIEW,
|
|
682
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
683
|
+
draftGate,
|
|
684
|
+
preApprovalGate,
|
|
685
|
+
allowedNextActions,
|
|
686
|
+
forbiddenActions,
|
|
687
|
+
nextAction: PR_CHECKPOINT_ACTION.WAIT_FOR_CI,
|
|
688
|
+
reason: "The PR is still draft, and this repo requires green current-head CI before entering `draft_gate`, so wait for CI to settle green before running the draft gate.",
|
|
689
|
+
mergeStateStatus,
|
|
690
|
+
conflictFiles,
|
|
691
|
+
refinementArtifact,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE]);
|
|
697
|
+
if (draftGate.currentHeadClean) {
|
|
698
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW]);
|
|
699
|
+
}
|
|
700
|
+
pushUnique(forbiddenActions, draftReviewForbidden);
|
|
701
|
+
|
|
702
|
+
return buildResult({
|
|
703
|
+
repo: input.repo ?? null,
|
|
704
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
705
|
+
currentHeadSha,
|
|
706
|
+
lifecycleState: lifecycleState || STATE.PR_DRAFT,
|
|
707
|
+
loopDisposition: loopDisposition ?? DISPOSITION.ACTION_REQUIRED,
|
|
708
|
+
gateBoundary: PR_CHECKPOINT.DRAFT_REVIEW,
|
|
709
|
+
draftGateAlreadySatisfied,
|
|
710
|
+
draftGate,
|
|
711
|
+
preApprovalGate,
|
|
712
|
+
allowedNextActions,
|
|
713
|
+
forbiddenActions,
|
|
714
|
+
nextAction: draftGate.currentHeadClean ? PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW : PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
715
|
+
reason: draftGate.currentHeadClean
|
|
716
|
+
? "The PR is still draft, and current-head clean `draft_gate` evidence exists, so `gh pr ready` is now legal."
|
|
717
|
+
: (draftGateRequireCi
|
|
718
|
+
? "The PR is still draft, current-head CI is green, and `draft_gate` is now the legal gate boundary before `gh pr ready`."
|
|
719
|
+
: "The PR is still draft, and this repo does not require CI before `draft_gate`, so the draft gate is the next legal boundary before `gh pr ready`."),
|
|
720
|
+
mergeStateStatus,
|
|
721
|
+
conflictFiles,
|
|
722
|
+
refinementArtifact,
|
|
723
|
+
copilotReviewRoundCount,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const postDraftForbidden = [
|
|
728
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
729
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
730
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
731
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
732
|
+
];
|
|
733
|
+
|
|
734
|
+
const internalOnlyPostDraftForbidden = [
|
|
735
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
736
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
737
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
738
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
739
|
+
];
|
|
740
|
+
|
|
741
|
+
if (effectiveLifecycleState === STATE.PR_READY_NO_FEEDBACK) {
|
|
742
|
+
if (reviewMode === "internal_only") {
|
|
743
|
+
// Explicitly internal-only PR: skip the external Copilot review cycle
|
|
744
|
+
if (ciStatus === "failure" || ciStatus === "crediblyGreen") {
|
|
745
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_BLOCKED]);
|
|
746
|
+
pushUnique(forbiddenActions, internalOnlyPostDraftForbidden);
|
|
747
|
+
return buildResult({
|
|
748
|
+
repo: input.repo ?? null,
|
|
749
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
750
|
+
currentHeadSha,
|
|
751
|
+
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
752
|
+
loopDisposition: DISPOSITION.BLOCKED,
|
|
753
|
+
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
754
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
755
|
+
draftGate,
|
|
756
|
+
preApprovalGate,
|
|
757
|
+
allowedNextActions,
|
|
758
|
+
forbiddenActions,
|
|
759
|
+
nextAction: PR_CHECKPOINT_ACTION.REPORT_BLOCKED,
|
|
760
|
+
reason: ciStatus === "crediblyGreen"
|
|
761
|
+
? "The current head has unconfirmed CI (credibly green), so gate progression remains blocked until CI is confirmed green."
|
|
762
|
+
: "The current head has failing CI, so gate progression remains blocked until the failing checks are fixed and revalidated.",
|
|
763
|
+
mergeStateStatus,
|
|
764
|
+
conflictFiles,
|
|
765
|
+
refinementArtifact,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (preApprovalGate.currentHeadClean) {
|
|
769
|
+
if (requireRetrospectiveGate) {
|
|
770
|
+
const retrospectiveGate = evaluateRetrospectiveMergeApproval(retrospectiveCheckpoint);
|
|
771
|
+
if (!retrospectiveGate.approved) {
|
|
772
|
+
return buildRetrospectiveGatePendingResult({
|
|
773
|
+
input,
|
|
774
|
+
currentHeadSha,
|
|
775
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
776
|
+
draftGate,
|
|
777
|
+
preApprovalGate,
|
|
778
|
+
mergeStateStatus,
|
|
779
|
+
conflictFiles,
|
|
780
|
+
reason: `Merge remains blocked: retrospective_gate_pending. ${retrospectiveGate.reason}`,
|
|
781
|
+
refinementArtifact,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
if (!draftGate.cleanEvidenceExists) {
|
|
788
|
+
return buildDraftGateNeededForMergeResult({
|
|
789
|
+
input,
|
|
790
|
+
currentHeadSha,
|
|
791
|
+
draftGate,
|
|
792
|
+
preApprovalGate,
|
|
793
|
+
mergeStateStatus,
|
|
794
|
+
conflictFiles,
|
|
795
|
+
underlyingReason: "Internal-only PR reached pre_approval_gate clean but has no clean draft_gate evidence.",
|
|
796
|
+
refinementArtifact,
|
|
797
|
+
effectiveLifecycleState,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL]);
|
|
802
|
+
pushUnique(forbiddenActions, internalOnlyPostDraftForbidden);
|
|
803
|
+
return buildResult({
|
|
804
|
+
repo: input.repo ?? null,
|
|
805
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
806
|
+
currentHeadSha,
|
|
807
|
+
lifecycleState: effectiveLifecycleState,
|
|
808
|
+
loopDisposition: loopDisposition ?? DISPOSITION.CLEAN_CONVERGED,
|
|
809
|
+
gateBoundary: PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
810
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
811
|
+
draftGate,
|
|
812
|
+
preApprovalGate,
|
|
813
|
+
allowedNextActions,
|
|
814
|
+
forbiddenActions,
|
|
815
|
+
nextAction: PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
816
|
+
reason: "This is an explicitly internal-only PR with clean draft_gate evidence and current-head clean pre_approval_gate, so it is ready for final human approval.",
|
|
817
|
+
mergeStateStatus,
|
|
818
|
+
conflictFiles,
|
|
819
|
+
refinementArtifact,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE]);
|
|
824
|
+
pushUnique(forbiddenActions, internalOnlyPostDraftForbidden);
|
|
825
|
+
return buildResult({
|
|
826
|
+
repo: input.repo ?? null,
|
|
827
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
828
|
+
currentHeadSha,
|
|
829
|
+
lifecycleState: effectiveLifecycleState,
|
|
830
|
+
loopDisposition: loopDisposition ?? DISPOSITION.ACTION_REQUIRED,
|
|
831
|
+
gateBoundary: PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
832
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
833
|
+
draftGate,
|
|
834
|
+
preApprovalGate,
|
|
835
|
+
allowedNextActions,
|
|
836
|
+
forbiddenActions,
|
|
837
|
+
nextAction: PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
838
|
+
reason: "This is an explicitly internal-only PR, so `pre_approval_gate` is the next legal boundary instead of an external Copilot review cycle.",
|
|
839
|
+
mergeStateStatus,
|
|
840
|
+
conflictFiles,
|
|
841
|
+
refinementArtifact,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW]);
|
|
846
|
+
pushUnique(forbiddenActions, postDraftForbidden);
|
|
847
|
+
return buildResult({
|
|
848
|
+
repo: input.repo ?? null,
|
|
849
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
850
|
+
currentHeadSha,
|
|
851
|
+
lifecycleState: effectiveLifecycleState,
|
|
852
|
+
loopDisposition: loopDisposition ?? DISPOSITION.ACTION_REQUIRED,
|
|
853
|
+
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
854
|
+
draftGateAlreadySatisfied,
|
|
855
|
+
draftGate,
|
|
856
|
+
preApprovalGate,
|
|
857
|
+
allowedNextActions,
|
|
858
|
+
forbiddenActions,
|
|
859
|
+
nextAction: PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
860
|
+
reason: "The PR is ready for review but the post-draft external review cycle has not started yet; request Copilot review before any `pre_approval_gate` entry.",
|
|
861
|
+
mergeStateStatus,
|
|
862
|
+
conflictFiles,
|
|
863
|
+
refinementArtifact,
|
|
864
|
+
copilotReviewRoundCount,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (effectiveLifecycleState === STATE.WAITING_FOR_COPILOT_REVIEW || effectiveLifecycleState === STATE.WAITING_FOR_CI) {
|
|
869
|
+
const waitAction = effectiveLifecycleState === STATE.WAITING_FOR_CI
|
|
870
|
+
? PR_CHECKPOINT_ACTION.WAIT_FOR_CI
|
|
871
|
+
: PR_CHECKPOINT_ACTION.WAIT_FOR_COPILOT_REVIEW;
|
|
872
|
+
|
|
873
|
+
pushUnique(allowedNextActions, [waitAction]);
|
|
874
|
+
pushUnique(forbiddenActions, postDraftForbidden);
|
|
875
|
+
return buildResult({
|
|
876
|
+
repo: input.repo ?? null,
|
|
877
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
878
|
+
currentHeadSha,
|
|
879
|
+
lifecycleState: effectiveLifecycleState,
|
|
880
|
+
loopDisposition: loopDisposition ?? DISPOSITION.PENDING,
|
|
881
|
+
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
882
|
+
draftGateAlreadySatisfied,
|
|
883
|
+
draftGate,
|
|
884
|
+
preApprovalGate,
|
|
885
|
+
allowedNextActions,
|
|
886
|
+
forbiddenActions,
|
|
887
|
+
nextAction: waitAction,
|
|
888
|
+
reason: effectiveLifecycleState === STATE.WAITING_FOR_CI
|
|
889
|
+
? "The post-draft review cycle is waiting on current-head CI, so `pre_approval_gate` remains illegal until CI settles cleanly."
|
|
890
|
+
: "The post-draft review cycle is still pending on Copilot review, so `pre_approval_gate` remains illegal until the current-head review cycle settles.",
|
|
891
|
+
mergeStateStatus,
|
|
892
|
+
conflictFiles,
|
|
893
|
+
refinementArtifact,
|
|
894
|
+
copilotReviewRoundCount,
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (effectiveLifecycleState === STATE.UNRESOLVED_FEEDBACK_PRESENT) {
|
|
899
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.ADDRESS_REVIEW_FEEDBACK]);
|
|
900
|
+
pushUnique(forbiddenActions, postDraftForbidden);
|
|
901
|
+
return buildResult({
|
|
902
|
+
repo: input.repo ?? null,
|
|
903
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
904
|
+
currentHeadSha,
|
|
905
|
+
lifecycleState: effectiveLifecycleState,
|
|
906
|
+
loopDisposition: loopDisposition ?? DISPOSITION.UNRESOLVED_FEEDBACK,
|
|
907
|
+
gateBoundary: PR_CHECKPOINT.FEEDBACK_RESOLUTION,
|
|
908
|
+
draftGateAlreadySatisfied,
|
|
909
|
+
draftGate,
|
|
910
|
+
preApprovalGate,
|
|
911
|
+
allowedNextActions,
|
|
912
|
+
forbiddenActions,
|
|
913
|
+
nextAction: PR_CHECKPOINT_ACTION.ADDRESS_REVIEW_FEEDBACK,
|
|
914
|
+
reason: "Actionable unresolved feedback exists, so follow-up work must stay in the review/fix cycle and cannot enter `pre_approval_gate` yet.",
|
|
915
|
+
mergeStateStatus,
|
|
916
|
+
conflictFiles,
|
|
917
|
+
refinementArtifact,
|
|
918
|
+
copilotReviewRoundCount,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (effectiveLifecycleState === STATE.ALREADY_FIXED_NEEDS_REPLY_RESOLVE) {
|
|
923
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPLY_RESOLVE_REVIEW_THREADS]);
|
|
924
|
+
pushUnique(forbiddenActions, postDraftForbidden);
|
|
925
|
+
return buildResult({
|
|
926
|
+
repo: input.repo ?? null,
|
|
927
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
928
|
+
currentHeadSha,
|
|
929
|
+
lifecycleState: effectiveLifecycleState,
|
|
930
|
+
loopDisposition: loopDisposition ?? DISPOSITION.UNRESOLVED_FEEDBACK,
|
|
931
|
+
gateBoundary: PR_CHECKPOINT.FEEDBACK_RESOLUTION,
|
|
932
|
+
draftGateAlreadySatisfied,
|
|
933
|
+
draftGate,
|
|
934
|
+
preApprovalGate,
|
|
935
|
+
allowedNextActions,
|
|
936
|
+
forbiddenActions,
|
|
937
|
+
nextAction: PR_CHECKPOINT_ACTION.REPLY_RESOLVE_REVIEW_THREADS,
|
|
938
|
+
reason: "Fixes were applied, but unresolved threads still need reply/resolve handling before another gate boundary is legal.",
|
|
939
|
+
mergeStateStatus,
|
|
940
|
+
conflictFiles,
|
|
941
|
+
refinementArtifact,
|
|
942
|
+
copilotReviewRoundCount,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (effectiveLifecycleState === STATE.READY_TO_REREQUEST_REVIEW) {
|
|
947
|
+
if (ciStatus === "failure" || ciStatus === "crediblyGreen") {
|
|
948
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_BLOCKED]);
|
|
949
|
+
pushUnique(forbiddenActions, postDraftForbidden);
|
|
950
|
+
return buildResult({
|
|
951
|
+
repo: input.repo ?? null,
|
|
952
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
953
|
+
currentHeadSha,
|
|
954
|
+
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
955
|
+
loopDisposition: DISPOSITION.BLOCKED,
|
|
956
|
+
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
957
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
958
|
+
draftGate,
|
|
959
|
+
preApprovalGate,
|
|
960
|
+
allowedNextActions,
|
|
961
|
+
forbiddenActions,
|
|
962
|
+
nextAction: PR_CHECKPOINT_ACTION.REPORT_BLOCKED,
|
|
963
|
+
reason: ciStatus === "crediblyGreen"
|
|
964
|
+
? "The current head has unconfirmed CI (credibly green), so gate progression remains blocked until CI is confirmed green."
|
|
965
|
+
: "The current head still has failing CI, so gate progression remains blocked until the failing checks are fixed and revalidated.",
|
|
966
|
+
mergeStateStatus,
|
|
967
|
+
conflictFiles,
|
|
968
|
+
refinementArtifact,
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (ciStatus === "pending" || ciStatus === "none") {
|
|
973
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.WAIT_FOR_CI]);
|
|
974
|
+
pushUnique(forbiddenActions, postDraftForbidden);
|
|
975
|
+
return buildResult({
|
|
976
|
+
repo: input.repo ?? null,
|
|
977
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
978
|
+
currentHeadSha,
|
|
979
|
+
lifecycleState: STATE.WAITING_FOR_CI,
|
|
980
|
+
loopDisposition: DISPOSITION.PENDING,
|
|
981
|
+
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
982
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
983
|
+
draftGate,
|
|
984
|
+
preApprovalGate,
|
|
985
|
+
allowedNextActions,
|
|
986
|
+
forbiddenActions,
|
|
987
|
+
nextAction: PR_CHECKPOINT_ACTION.WAIT_FOR_CI,
|
|
988
|
+
reason: "The current head does not yet have green or credibly green CI, so `pre_approval_gate` remains illegal until CI settles.",
|
|
989
|
+
mergeStateStatus,
|
|
990
|
+
conflictFiles,
|
|
991
|
+
refinementArtifact,
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const roundExhaustionGateEvidenceNote = roundCapReached
|
|
996
|
+
? buildRoundExhaustionGateEvidenceNote({ copilotReviewRoundCount, maxCopilotRounds })
|
|
997
|
+
: null;
|
|
998
|
+
|
|
999
|
+
if (!sameHeadCleanConverged && !roundCapReached) {
|
|
1000
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REREQUEST_COPILOT_REVIEW]);
|
|
1001
|
+
pushUnique(forbiddenActions, postDraftForbidden);
|
|
1002
|
+
return buildResult({
|
|
1003
|
+
repo: input.repo ?? null,
|
|
1004
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1005
|
+
currentHeadSha,
|
|
1006
|
+
lifecycleState: effectiveLifecycleState,
|
|
1007
|
+
loopDisposition: loopDisposition ?? DISPOSITION.ACTION_REQUIRED,
|
|
1008
|
+
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
1009
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
1010
|
+
draftGate,
|
|
1011
|
+
preApprovalGate,
|
|
1012
|
+
allowedNextActions,
|
|
1013
|
+
forbiddenActions,
|
|
1014
|
+
nextAction: PR_CHECKPOINT_ACTION.REREQUEST_COPILOT_REVIEW,
|
|
1015
|
+
reason: "The review loop is between passes, but the current head does not yet have a clean settled Copilot convergence point, so `pre_approval_gate` is still forbidden.",
|
|
1016
|
+
mergeStateStatus,
|
|
1017
|
+
conflictFiles,
|
|
1018
|
+
refinementArtifact,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (preApprovalGate.currentHeadClean) {
|
|
1023
|
+
if (requireRetrospectiveGate) {
|
|
1024
|
+
const retrospectiveGate = evaluateRetrospectiveMergeApproval(retrospectiveCheckpoint);
|
|
1025
|
+
if (!retrospectiveGate.approved) {
|
|
1026
|
+
return buildRetrospectiveGatePendingResult({
|
|
1027
|
+
input,
|
|
1028
|
+
currentHeadSha,
|
|
1029
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
1030
|
+
draftGate,
|
|
1031
|
+
preApprovalGate,
|
|
1032
|
+
mergeStateStatus,
|
|
1033
|
+
conflictFiles,
|
|
1034
|
+
reason: `Merge remains blocked: retrospective_gate_pending. ${retrospectiveGate.reason}`,
|
|
1035
|
+
refinementArtifact,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
if (!draftGate.cleanEvidenceExists && !roundCapReached) {
|
|
1042
|
+
return buildDraftGateNeededForMergeResult({
|
|
1043
|
+
input,
|
|
1044
|
+
currentHeadSha,
|
|
1045
|
+
draftGate,
|
|
1046
|
+
preApprovalGate,
|
|
1047
|
+
mergeStateStatus,
|
|
1048
|
+
conflictFiles,
|
|
1049
|
+
underlyingReason: "Converged PR has clean pre_approval_gate but no clean draft_gate evidence.",
|
|
1050
|
+
refinementArtifact,
|
|
1051
|
+
effectiveLifecycleState,
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL]);
|
|
1056
|
+
pushUnique(forbiddenActions, [
|
|
1057
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
1058
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
1059
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
1060
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
1061
|
+
]);
|
|
1062
|
+
return buildResult({
|
|
1063
|
+
repo: input.repo ?? null,
|
|
1064
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1065
|
+
currentHeadSha,
|
|
1066
|
+
lifecycleState: effectiveLifecycleState,
|
|
1067
|
+
loopDisposition: loopDisposition ?? DISPOSITION.CLEAN_CONVERGED,
|
|
1068
|
+
gateBoundary: PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
1069
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
1070
|
+
draftGate,
|
|
1071
|
+
preApprovalGate,
|
|
1072
|
+
allowedNextActions,
|
|
1073
|
+
forbiddenActions,
|
|
1074
|
+
nextAction: PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
1075
|
+
reason: roundCapReached
|
|
1076
|
+
? `Round-cap clean fallback accepted as draft gate equivalent (${copilotReviewRoundCount}/${maxCopilotRounds} rounds, zero unresolved threads, ${ciStatus === "crediblyGreen" ? "credibly green" : "green"} CI). The current head has clean \`pre_approval_gate\` evidence, so the PR is at the final approval boundary.`
|
|
1077
|
+
: (ciStatus === "crediblyGreen"
|
|
1078
|
+
? "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."
|
|
1079
|
+
: "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."),
|
|
1080
|
+
mergeStateStatus,
|
|
1081
|
+
conflictFiles,
|
|
1082
|
+
refinementArtifact,
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE]);
|
|
1087
|
+
pushUnique(forbiddenActions, [
|
|
1088
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
1089
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
1090
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
1091
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
1092
|
+
]);
|
|
1093
|
+
return buildResult({
|
|
1094
|
+
repo: input.repo ?? null,
|
|
1095
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1096
|
+
currentHeadSha,
|
|
1097
|
+
lifecycleState: effectiveLifecycleState,
|
|
1098
|
+
loopDisposition: loopDisposition ?? DISPOSITION.CLEAN_CONVERGED,
|
|
1099
|
+
gateBoundary: PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
1100
|
+
draftGateAlreadySatisfied,
|
|
1101
|
+
draftGate,
|
|
1102
|
+
preApprovalGate,
|
|
1103
|
+
allowedNextActions,
|
|
1104
|
+
forbiddenActions,
|
|
1105
|
+
nextAction: PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
1106
|
+
reason: roundCapReached
|
|
1107
|
+
? `The Copilot round limit is exhausted (${copilotReviewRoundCount}/${maxCopilotRounds}), and the current head has zero unresolved threads with ${ciStatus === "crediblyGreen" ? "credibly green" : "green"} CI, so \`pre_approval_gate\` fallback is now the next legal boundary.`
|
|
1108
|
+
: (ciStatus === "crediblyGreen"
|
|
1109
|
+
? "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."
|
|
1110
|
+
: "The current head has a clean settled post-draft review cycle, so `pre_approval_gate` is now the next legal boundary."),
|
|
1111
|
+
mergeStateStatus,
|
|
1112
|
+
conflictFiles,
|
|
1113
|
+
gateEvidenceNote: roundCapReached ? roundExhaustionGateEvidenceNote : null,
|
|
1114
|
+
copilotReviewRoundCount,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (effectiveLifecycleState === STATE.LOW_SIGNAL_CONVERGED) {
|
|
1119
|
+
if (ciStatus === "failure" || ciStatus === "crediblyGreen") {
|
|
1120
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_BLOCKED]);
|
|
1121
|
+
pushUnique(forbiddenActions, postDraftForbidden);
|
|
1122
|
+
return buildResult({
|
|
1123
|
+
repo: input.repo ?? null,
|
|
1124
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1125
|
+
currentHeadSha,
|
|
1126
|
+
lifecycleState: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
1127
|
+
loopDisposition: DISPOSITION.BLOCKED,
|
|
1128
|
+
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
1129
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
1130
|
+
draftGate,
|
|
1131
|
+
preApprovalGate,
|
|
1132
|
+
allowedNextActions,
|
|
1133
|
+
forbiddenActions,
|
|
1134
|
+
nextAction: PR_CHECKPOINT_ACTION.REPORT_BLOCKED,
|
|
1135
|
+
reason: ciStatus === "crediblyGreen"
|
|
1136
|
+
? "The low-signal heuristic indicates convergence, but the current head has unconfirmed CI (credibly green), so gate progression remains blocked until CI is confirmed green."
|
|
1137
|
+
: "The low-signal heuristic indicates convergence, but the current head still has failing CI, so gate progression remains blocked.",
|
|
1138
|
+
mergeStateStatus,
|
|
1139
|
+
conflictFiles,
|
|
1140
|
+
refinementArtifact,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
if (ciStatus === "pending" || ciStatus === "none") {
|
|
1144
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.WAIT_FOR_CI]);
|
|
1145
|
+
pushUnique(forbiddenActions, postDraftForbidden);
|
|
1146
|
+
return buildResult({
|
|
1147
|
+
repo: input.repo ?? null,
|
|
1148
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1149
|
+
currentHeadSha,
|
|
1150
|
+
lifecycleState: STATE.WAITING_FOR_CI,
|
|
1151
|
+
loopDisposition: DISPOSITION.PENDING,
|
|
1152
|
+
gateBoundary: PR_CHECKPOINT.POST_DRAFT_EXTERNAL_REVIEW,
|
|
1153
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
1154
|
+
draftGate,
|
|
1155
|
+
preApprovalGate,
|
|
1156
|
+
allowedNextActions,
|
|
1157
|
+
forbiddenActions,
|
|
1158
|
+
nextAction: PR_CHECKPOINT_ACTION.WAIT_FOR_CI,
|
|
1159
|
+
reason: "The low-signal heuristic indicates convergence, but the current head does not yet have green or credibly green CI.",
|
|
1160
|
+
mergeStateStatus,
|
|
1161
|
+
conflictFiles,
|
|
1162
|
+
refinementArtifact,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
if (preApprovalGate.currentHeadClean) {
|
|
1166
|
+
if (requireRetrospectiveGate) {
|
|
1167
|
+
const retrospectiveGate = evaluateRetrospectiveMergeApproval(retrospectiveCheckpoint);
|
|
1168
|
+
if (!retrospectiveGate.approved) {
|
|
1169
|
+
return buildRetrospectiveGatePendingResult({
|
|
1170
|
+
input,
|
|
1171
|
+
currentHeadSha,
|
|
1172
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
1173
|
+
draftGate,
|
|
1174
|
+
preApprovalGate,
|
|
1175
|
+
mergeStateStatus,
|
|
1176
|
+
conflictFiles,
|
|
1177
|
+
reason: `Merge remains blocked: retrospective_gate_pending. ${retrospectiveGate.reason}`,
|
|
1178
|
+
refinementArtifact,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
if (!draftGate.cleanEvidenceExists) {
|
|
1185
|
+
return buildDraftGateNeededForMergeResult({
|
|
1186
|
+
input,
|
|
1187
|
+
currentHeadSha,
|
|
1188
|
+
draftGate,
|
|
1189
|
+
preApprovalGate,
|
|
1190
|
+
mergeStateStatus,
|
|
1191
|
+
conflictFiles,
|
|
1192
|
+
underlyingReason: "Low-signal converged PR has clean pre_approval_gate but no clean draft_gate evidence.",
|
|
1193
|
+
refinementArtifact,
|
|
1194
|
+
effectiveLifecycleState,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL]);
|
|
1199
|
+
pushUnique(forbiddenActions, [
|
|
1200
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
1201
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
1202
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
1203
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
1204
|
+
]);
|
|
1205
|
+
return buildResult({
|
|
1206
|
+
repo: input.repo ?? null,
|
|
1207
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1208
|
+
currentHeadSha,
|
|
1209
|
+
lifecycleState: effectiveLifecycleState,
|
|
1210
|
+
loopDisposition: DISPOSITION.DONE,
|
|
1211
|
+
gateBoundary: PR_CHECKPOINT.FINAL_APPROVAL_READY,
|
|
1212
|
+
draftGateAlreadySatisfied: roundCapReached ? true : draftGateAlreadySatisfied,
|
|
1213
|
+
draftGate,
|
|
1214
|
+
preApprovalGate,
|
|
1215
|
+
allowedNextActions,
|
|
1216
|
+
forbiddenActions,
|
|
1217
|
+
nextAction: PR_CHECKPOINT_ACTION.AWAIT_FINAL_HUMAN_APPROVAL,
|
|
1218
|
+
reason: "Low-signal heuristic indicates convergence and current-head clean pre_approval_gate evidence exists.",
|
|
1219
|
+
mergeStateStatus,
|
|
1220
|
+
conflictFiles,
|
|
1221
|
+
refinementArtifact,
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE]);
|
|
1225
|
+
pushUnique(forbiddenActions, [
|
|
1226
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
1227
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
1228
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
1229
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
1230
|
+
]);
|
|
1231
|
+
return buildResult({
|
|
1232
|
+
repo: input.repo ?? null,
|
|
1233
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1234
|
+
currentHeadSha,
|
|
1235
|
+
lifecycleState: effectiveLifecycleState,
|
|
1236
|
+
loopDisposition: DISPOSITION.DONE,
|
|
1237
|
+
gateBoundary: PR_CHECKPOINT.PRE_APPROVAL_GATE_WINDOW,
|
|
1238
|
+
draftGateAlreadySatisfied,
|
|
1239
|
+
draftGate,
|
|
1240
|
+
preApprovalGate,
|
|
1241
|
+
allowedNextActions,
|
|
1242
|
+
forbiddenActions,
|
|
1243
|
+
nextAction: PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
1244
|
+
reason: "Low-signal heuristic indicates convergence (diminishing-returns signal detected), routing to pre_approval_gate instead of re-requesting Copilot.",
|
|
1245
|
+
mergeStateStatus,
|
|
1246
|
+
conflictFiles,
|
|
1247
|
+
refinementArtifact,
|
|
1248
|
+
copilotReviewRoundCount,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REPORT_BLOCKED]);
|
|
1253
|
+
pushUnique(forbiddenActions, [
|
|
1254
|
+
PR_CHECKPOINT_ACTION.RUN_DRAFT_GATE,
|
|
1255
|
+
PR_CHECKPOINT_ACTION.MARK_READY_FOR_REVIEW,
|
|
1256
|
+
PR_CHECKPOINT_ACTION.REQUEST_COPILOT_REVIEW,
|
|
1257
|
+
PR_CHECKPOINT_ACTION.RUN_PRE_APPROVAL_GATE,
|
|
1258
|
+
PR_CHECKPOINT_ACTION.DECLARE_MERGE_READY,
|
|
1259
|
+
]);
|
|
1260
|
+
return buildResult({
|
|
1261
|
+
repo: input.repo ?? null,
|
|
1262
|
+
pr: Number.isInteger(input.pr) ? input.pr : null,
|
|
1263
|
+
currentHeadSha,
|
|
1264
|
+
lifecycleState: effectiveLifecycleState,
|
|
1265
|
+
loopDisposition: loopDisposition ?? DISPOSITION.BLOCKED,
|
|
1266
|
+
gateBoundary: PR_CHECKPOINT.BLOCKED,
|
|
1267
|
+
draftGateAlreadySatisfied,
|
|
1268
|
+
draftGate,
|
|
1269
|
+
preApprovalGate,
|
|
1270
|
+
allowedNextActions,
|
|
1271
|
+
forbiddenActions,
|
|
1272
|
+
nextAction: PR_CHECKPOINT_ACTION.REPORT_BLOCKED,
|
|
1273
|
+
reason: "The PR gate-boundary evaluator could not map this lifecycle state to a legal gate transition; reconcile before continuing.",
|
|
1274
|
+
mergeStateStatus,
|
|
1275
|
+
conflictFiles,
|
|
1276
|
+
refinementArtifact,
|
|
1277
|
+
});
|
|
1278
|
+
}
|