@dev-loops/core 1.0.4-pre.0 → 1.0.4-pre.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/analysis/change-classifier.mjs +16 -30
- package/src/analysis/diff-analyzer.mjs +38 -15
- package/src/claude/asset-generation.mjs +36 -1
- package/src/config/config.mjs +158 -81
- package/src/config/extension-defaults.yaml +18 -6
- package/src/github/copilot-helpers.mjs +22 -8
- package/src/github/gh.mjs +14 -1
- package/src/github/review-threads.mjs +6 -0
- package/src/loop/copilot-loop-state.mjs +44 -4
- package/src/loop/finding-cluster.mjs +9 -9
- package/src/loop/gate-fanin.mjs +21 -0
- package/src/loop/main-checkout-ff.mjs +169 -17
- package/src/loop/merge-approval.mjs +122 -26
- package/src/loop/pr-gate-coordination.mjs +283 -62
- package/src/loop/refinement-grill-state.mjs +120 -8
- package/src/loop/review-operation.mjs +161 -0
- package/src/loop/spec-authority.mjs +21 -0
- package/src/projects/move-queue-item.mjs +5 -79
- package/src/projects/projects-access.mjs +124 -0
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* precondition, and the aggregate naming.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { isCopilotLogin, classifyCopilotReviewBodyDisposition, COPILOT_DISPOSITION, SUBMITTED_REVIEW_STATES } from "../github/copilot-helpers.mjs";
|
|
23
|
+
import { isCopilotLogin, classifyCopilotReviewBodyDisposition, COPILOT_DISPOSITION, SUBMITTED_REVIEW_STATES, extractReviewCommitSha } from "../github/copilot-helpers.mjs";
|
|
24
24
|
import { findBlockingTitleMarkers } from "./pr-title-markers.mjs";
|
|
25
25
|
import { resolveSizeBudgetHumanApprovalRequired } from "./size-budget-merge-gate.mjs";
|
|
26
26
|
import { deriveLoopCiStatusFromRollup } from "./copilot-ci-status.mjs";
|
|
@@ -84,6 +84,15 @@ function reviewCommit(entry) {
|
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Copilot-only login extraction for evaluateCopilotConvergence: accepts the
|
|
88
|
+
// `gh pr view` GraphQL shape (author.login) in addition to the REST shape
|
|
89
|
+
// (user.login | login) reviewLogin reads. Scoped to the Copilot-convergence
|
|
90
|
+
// eval so verifyFreshHumanApproval's human-approval matching is unchanged.
|
|
91
|
+
function copilotConvergenceReviewLogin(entry) {
|
|
92
|
+
if (typeof entry?.author?.login === "string" && entry.author.login.length > 0) return entry.author.login;
|
|
93
|
+
return reviewLogin(entry);
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
/**
|
|
88
97
|
* Verify a fresh, agent-unforgeable, head-pinned human approval by `approvedBy`.
|
|
89
98
|
*
|
|
@@ -153,13 +162,25 @@ export function verifyFreshHumanApproval({ approvedBy, currentHeadSha, reviews =
|
|
|
153
162
|
};
|
|
154
163
|
}
|
|
155
164
|
|
|
165
|
+
export const COPILOT_CONVERGENCE_STATE = Object.freeze({
|
|
166
|
+
CURRENT_HEAD_CLEAN: "current_head_clean",
|
|
167
|
+
CURRENT_HEAD_FINDINGS: "current_head_findings",
|
|
168
|
+
NO_CURRENT_HEAD_REVIEW: "no_current_head_review",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/** ADR 0012 end states (amended by ADR 0090) that may satisfy convergence without a current-head review. */
|
|
172
|
+
export const COPILOT_ABSENT_REVIEW_DISPOSITION = Object.freeze({
|
|
173
|
+
CONVERGED_ONCE: "converged_once",
|
|
174
|
+
ROUND_CAP_CLEAN_FALLBACK: "round_cap_clean_fallback",
|
|
175
|
+
DOCS_ONLY_SUPPRESSION: "docs_only_suppression",
|
|
176
|
+
COPILOT_GATE_DISABLED: "copilot_gate_disabled",
|
|
177
|
+
});
|
|
178
|
+
const SANCTIONED_ABSENT_REVIEW_DISPOSITIONS = new Set(Object.values(COPILOT_ABSENT_REVIEW_DISPOSITION));
|
|
179
|
+
|
|
156
180
|
/**
|
|
157
|
-
* Copilot-convergence
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* (`classifyCopilotReviewBodyDisposition`). The classification is shared, so it
|
|
161
|
-
* cannot drift; the POLICY differs by design (the loop self-blocks on 🔵, this
|
|
162
|
-
* gate treats 🔵 as conductor-overridable). Fail-closed.
|
|
181
|
+
* Shared Copilot-convergence evaluation for pre-approval entry and merge.
|
|
182
|
+
* Both treat a thread-clean 🔵 as conductor-overridable; unresolved threads
|
|
183
|
+
* still block independently. Fail-closed.
|
|
163
184
|
*
|
|
164
185
|
* Only the LATEST Copilot review pinned to `currentHeadSha` is judged, so a
|
|
165
186
|
* stale non-approval at an earlier head never blocks and a later same-head 🟢
|
|
@@ -175,16 +196,28 @@ export function verifyFreshHumanApproval({ approvedBy, currentHeadSha, reviews =
|
|
|
175
196
|
* threads — the conductor's override is choosing to run the merge on a
|
|
176
197
|
* thread-clean 🔵.
|
|
177
198
|
* unrecognized disposition -> BLOCK (fail closed on a Copilot format change).
|
|
178
|
-
* 🟢 clean /
|
|
199
|
+
* 🟢 clean / headerless -> PASS.
|
|
179
200
|
*
|
|
180
|
-
*
|
|
201
|
+
* Three distinct states (`state` on the result):
|
|
202
|
+
* current_head_clean a current-head review passes (🟢 / headerless / 🔵);
|
|
203
|
+
* current_head_findings a current-head review blocks (🟡 / unrecognized);
|
|
204
|
+
* no_current_head_review no current-head Copilot review exists (none yet, or
|
|
205
|
+
* every review is on an earlier head). This state
|
|
206
|
+
* passes ONLY through a sanctioned disposition recorded
|
|
207
|
+
* for the current head (`absentReviewDisposition`
|
|
208
|
+
* `{ kind, headSha }`, kind in
|
|
209
|
+
* COPILOT_ABSENT_REVIEW_DISPOSITION). Absence alone never
|
|
210
|
+
* converges, and a stale earlier-head review is never
|
|
211
|
+
* inherited as the current-head verdict.
|
|
212
|
+
*
|
|
213
|
+
* @returns {{ ok: boolean, state: string|null, disposition: string|null, reason: string|null }}
|
|
181
214
|
*/
|
|
182
|
-
export function evaluateCopilotConvergence({ currentHeadSha = null, reviews = [] } = {}) {
|
|
215
|
+
export function evaluateCopilotConvergence({ currentHeadSha = null, reviews = [], absentReviewDisposition = null } = {}) {
|
|
183
216
|
const head = typeof currentHeadSha === "string" ? currentHeadSha.trim() : "";
|
|
184
217
|
// Head unknown: no current-head review can be pinned. Fail closed (matches
|
|
185
218
|
// verifyFreshHumanApproval), so this precondition can never pass without a
|
|
186
219
|
// known head to pin the Copilot disposition to.
|
|
187
|
-
if (head.length === 0) return { ok: false, disposition: null, reason: "current head SHA is unknown; cannot pin a Copilot review to it" };
|
|
220
|
+
if (head.length === 0) return { ok: false, state: null, disposition: null, reason: "current head SHA is unknown; cannot pin a Copilot review to it" };
|
|
188
221
|
|
|
189
222
|
// Mirror summarizeCopilotReviews' current-head finding selection BYTE-FOR-BYTE
|
|
190
223
|
// so the merge gate and the loop can never diverge (they already share
|
|
@@ -199,36 +232,65 @@ export function evaluateCopilotConvergence({ currentHeadSha = null, reviews = []
|
|
|
199
232
|
// array order never silently drops a finding.
|
|
200
233
|
let latestDisposition = null;
|
|
201
234
|
let latestAt = null;
|
|
235
|
+
// The id of the review that owns latestDisposition: the review a
|
|
236
|
+
// copilot-body-disposition record must name to clear a finding. Null when two
|
|
237
|
+
// tied reviews both block, so no single record can clear the tie.
|
|
238
|
+
let latestReviewId = null;
|
|
202
239
|
for (const entry of Array.isArray(reviews) ? reviews : []) {
|
|
203
|
-
|
|
240
|
+
// Shape-tolerant Copilot-login + commit extraction: the merge gate feeds
|
|
241
|
+
// REST-shaped reviews (user.login/commit_id) while the gate-ENTRY detector
|
|
242
|
+
// feeds `gh pr view` GraphQL-shaped reviews (author.login/commit.oid). Both
|
|
243
|
+
// call sites route through this one evaluateCopilotConvergence, so it must
|
|
244
|
+
// recognize BOTH shapes to produce ONE convergence verdict. Only Copilot
|
|
245
|
+
// reviews matter here, so broadening the login read to author.login cannot
|
|
246
|
+
// affect verifyFreshHumanApproval (which keeps its own REST-only
|
|
247
|
+
// reviewLogin/reviewCommit).
|
|
248
|
+
const login = copilotConvergenceReviewLogin(entry);
|
|
204
249
|
if (login === null || !isCopilotLogin(login)) continue;
|
|
205
|
-
if (
|
|
250
|
+
if (extractReviewCommitSha(entry) !== head) continue; // only current-head reviews
|
|
206
251
|
const state = typeof entry?.state === "string" ? entry.state.toUpperCase() : "";
|
|
207
252
|
if (state === "PENDING" || !SUBMITTED_REVIEW_STATES.has(state)) continue; // PENDING/unknown never sets the finding
|
|
208
253
|
const disposition = classifyCopilotReviewBodyDisposition(state, entry?.body);
|
|
254
|
+
const reviewId = entry?.id !== null && entry?.id !== undefined ? String(entry.id) : null;
|
|
209
255
|
const submittedAt = typeof entry?.submittedAt === "string"
|
|
210
256
|
? entry.submittedAt
|
|
211
257
|
: (typeof entry?.submitted_at === "string" ? entry.submitted_at : null);
|
|
212
258
|
if (submittedAt !== null && (latestAt === null || submittedAt > latestAt)) {
|
|
213
259
|
latestDisposition = disposition; // a lexicographically-later submittedAt supersedes (matches summarize)
|
|
260
|
+
latestReviewId = reviewId;
|
|
214
261
|
latestAt = submittedAt;
|
|
215
|
-
} else if (submittedAt
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
262
|
+
} else if (submittedAt === latestAt) {
|
|
263
|
+
// Equal-string tie, or both null.
|
|
264
|
+
({ disposition: latestDisposition, reviewId: latestReviewId } = foldTiedReview(
|
|
265
|
+
{ disposition: latestDisposition, reviewId: latestReviewId },
|
|
266
|
+
{ disposition, reviewId },
|
|
267
|
+
));
|
|
219
268
|
}
|
|
220
269
|
// a null submittedAt once a non-null latest exists is ignored (mirrors summarize)
|
|
221
270
|
}
|
|
222
|
-
if (latestDisposition === null)
|
|
271
|
+
if (latestDisposition === null) {
|
|
272
|
+
const absent = COPILOT_CONVERGENCE_STATE.NO_CURRENT_HEAD_REVIEW;
|
|
273
|
+
const kind = absentReviewDisposition?.kind;
|
|
274
|
+
if (SANCTIONED_ABSENT_REVIEW_DISPOSITIONS.has(kind) && absentReviewDisposition?.headSha === head) {
|
|
275
|
+
return { ok: true, state: absent, disposition: kind, reason: null };
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
state: absent,
|
|
280
|
+
disposition: null,
|
|
281
|
+
reason: `no current-head Copilot review exists on head ${head}; convergence requires a clean current-head review or a sanctioned disposition recorded for this head (${[...SANCTIONED_ABSENT_REVIEW_DISPOSITIONS].join(", ")})`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
223
284
|
|
|
285
|
+
const findings = COPILOT_CONVERGENCE_STATE.CURRENT_HEAD_FINDINGS;
|
|
224
286
|
if (latestDisposition === COPILOT_DISPOSITION.CHANGES_RECOMMENDED) {
|
|
225
|
-
return { ok: false, disposition: latestDisposition, reason: `current-head Copilot review is "Changes recommended" (🟡, actionable non-approval); converge to "Approval recommended" (🟢) or resolve the feedback before merge` };
|
|
287
|
+
return { ok: false, state: findings, disposition: latestDisposition, reviewId: latestReviewId, reason: `current-head Copilot review is "Changes recommended" (🟡, actionable non-approval); converge to "Approval recommended" (🟢) or resolve the feedback before merge` };
|
|
226
288
|
}
|
|
227
289
|
if (latestDisposition === COPILOT_DISPOSITION.UNRECOGNIZED) {
|
|
228
|
-
return { ok: false, disposition: latestDisposition, reason: `current-head Copilot review carries an unrecognized disposition header (fail closed); a recognized "Approval recommended" (🟢) is required` };
|
|
290
|
+
return { ok: false, state: findings, disposition: latestDisposition, reviewId: latestReviewId, reason: `current-head Copilot review carries an unrecognized disposition header (fail closed); a recognized "Approval recommended" (🟢) is required` };
|
|
229
291
|
}
|
|
230
292
|
// CLEAN, NONE, and NEEDS_CLOSER_LOOK (🔵, conductor-overridable) pass.
|
|
231
|
-
return { ok: true, disposition: latestDisposition, reason: null };
|
|
293
|
+
return { ok: true, state: COPILOT_CONVERGENCE_STATE.CURRENT_HEAD_CLEAN, disposition: latestDisposition, reviewId: latestReviewId, reason: null };
|
|
232
294
|
}
|
|
233
295
|
|
|
234
296
|
// Disposition blocking precedence, most-blocking first. Used to fold an
|
|
@@ -250,6 +312,19 @@ function moreBlockingDisposition(a, b) {
|
|
|
250
312
|
return ra <= rb ? a : b;
|
|
251
313
|
}
|
|
252
314
|
|
|
315
|
+
// Fold one tied current-head review into the running latest. The disposition
|
|
316
|
+
// folds toward the most blocking. The owning review id follows the blocking
|
|
317
|
+
// review; two tied blocking reviews leave no single owner (null).
|
|
318
|
+
const BLOCKING_CONVERGENCE_DISPOSITIONS = new Set([COPILOT_DISPOSITION.CHANGES_RECOMMENDED, COPILOT_DISPOSITION.UNRECOGNIZED]);
|
|
319
|
+
function foldTiedReview(latest, next) {
|
|
320
|
+
if (latest.disposition === null) return next;
|
|
321
|
+
if (BLOCKING_CONVERGENCE_DISPOSITIONS.has(latest.disposition) && BLOCKING_CONVERGENCE_DISPOSITIONS.has(next.disposition)) {
|
|
322
|
+
return { disposition: moreBlockingDisposition(latest.disposition, next.disposition), reviewId: null };
|
|
323
|
+
}
|
|
324
|
+
const disposition = moreBlockingDisposition(latest.disposition, next.disposition);
|
|
325
|
+
return { disposition, reviewId: disposition === latest.disposition ? latest.reviewId : next.reviewId };
|
|
326
|
+
}
|
|
327
|
+
|
|
253
328
|
/**
|
|
254
329
|
* Decide whether merge is authorized given the class, the standing
|
|
255
330
|
* authorization signal, and any fresh per-merge approval.
|
|
@@ -327,6 +402,12 @@ export function evaluateMergePreconditions({
|
|
|
327
402
|
comments = [],
|
|
328
403
|
standingAuthorized = false,
|
|
329
404
|
stableRelease = false,
|
|
405
|
+
copilotAbsentReviewDisposition = null,
|
|
406
|
+
copilotBodyDisposition = null,
|
|
407
|
+
// Refusal reason when a Copilot review submitted after the current-head
|
|
408
|
+
// verdict review sits on an earlier commit and is not converged (the latest
|
|
409
|
+
// review decides); null otherwise.
|
|
410
|
+
copilotLaterReviewRefusal = null,
|
|
330
411
|
} = {}) {
|
|
331
412
|
const failures = [];
|
|
332
413
|
|
|
@@ -375,9 +456,21 @@ export function evaluateMergePreconditions({
|
|
|
375
456
|
|
|
376
457
|
// Copilot-convergence precondition: refuse a current-head Copilot non-approval
|
|
377
458
|
// body disposition, mirroring the loop's copilotBodyFeedbackUnresolved.
|
|
378
|
-
|
|
379
|
-
|
|
459
|
+
// A trusted copilot-body-disposition record resolved for the current head
|
|
460
|
+
// (`{ headSha, reviewId, ... }`) clears a current-head finding, as it does
|
|
461
|
+
// at gate entry, only when it names the review that raised the finding.
|
|
462
|
+
const copilotConvergence = evaluateCopilotConvergence({ currentHeadSha, reviews, absentReviewDisposition: copilotAbsentReviewDisposition });
|
|
463
|
+
const head = typeof currentHeadSha === "string" ? currentHeadSha.trim().toLowerCase() : "";
|
|
464
|
+
const bodyCleared = copilotConvergence.state === COPILOT_CONVERGENCE_STATE.CURRENT_HEAD_FINDINGS
|
|
465
|
+
&& head.length > 0
|
|
466
|
+
&& typeof copilotBodyDisposition?.headSha === "string" && copilotBodyDisposition.headSha.toLowerCase() === head
|
|
467
|
+
// The record clears only the review that raised the current-head finding.
|
|
468
|
+
&& copilotConvergence.reviewId !== null
|
|
469
|
+
&& String(copilotBodyDisposition.reviewId) === copilotConvergence.reviewId;
|
|
470
|
+
if (!copilotConvergence.ok && !bodyCleared) {
|
|
380
471
|
failures.push({ precondition: "copilot_convergence", reason: copilotConvergence.reason });
|
|
472
|
+
} else if (typeof copilotLaterReviewRefusal === "string") {
|
|
473
|
+
failures.push({ precondition: "copilot_convergence", reason: copilotLaterReviewRefusal });
|
|
381
474
|
}
|
|
382
475
|
|
|
383
476
|
const mergeClass = resolveMergeClass({ sizeOutcome, touchesT1, stableRelease });
|
|
@@ -391,9 +484,12 @@ export function evaluateMergePreconditions({
|
|
|
391
484
|
failures,
|
|
392
485
|
mergeClass,
|
|
393
486
|
approvalVia: decision.authorized ? decision.via : null,
|
|
394
|
-
// Audit trace:
|
|
395
|
-
//
|
|
396
|
-
//
|
|
487
|
+
// Audit trace: which convergence state applied and what settled it (the
|
|
488
|
+
// current-head review disposition, or the sanctioned disposition kind when
|
|
489
|
+
// no current-head review exists), so a merge on a conductor-overridable 🔵
|
|
490
|
+
// or without a current-head review is recorded, never invisible.
|
|
491
|
+
copilotConvergenceState: copilotConvergence.state,
|
|
397
492
|
copilotDisposition: copilotConvergence.disposition,
|
|
493
|
+
copilotBodyDisposition: bodyCleared ? copilotBodyDisposition : null,
|
|
398
494
|
};
|
|
399
495
|
}
|