@dev-loops/core 1.0.4-pre.0 → 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 +16 -30
- package/src/analysis/diff-analyzer.mjs +38 -15
- package/src/claude/asset-generation.mjs +36 -1
- package/src/config/config.mjs +143 -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 +38 -2
- 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 +87 -22
- package/src/loop/pr-gate-coordination.mjs +270 -60
- 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,24 @@ 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 that may satisfy convergence without a current-head review. */
|
|
172
|
+
export const COPILOT_ABSENT_REVIEW_DISPOSITION = Object.freeze({
|
|
173
|
+
ROUND_CAP_CLEAN_FALLBACK: "round_cap_clean_fallback",
|
|
174
|
+
DOCS_ONLY_SUPPRESSION: "docs_only_suppression",
|
|
175
|
+
COPILOT_GATE_DISABLED: "copilot_gate_disabled",
|
|
176
|
+
});
|
|
177
|
+
const SANCTIONED_ABSENT_REVIEW_DISPOSITIONS = new Set(Object.values(COPILOT_ABSENT_REVIEW_DISPOSITION));
|
|
178
|
+
|
|
156
179
|
/**
|
|
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.
|
|
180
|
+
* Shared Copilot-convergence evaluation for pre-approval entry and merge.
|
|
181
|
+
* Both treat a thread-clean 🔵 as conductor-overridable; unresolved threads
|
|
182
|
+
* still block independently. Fail-closed.
|
|
163
183
|
*
|
|
164
184
|
* Only the LATEST Copilot review pinned to `currentHeadSha` is judged, so a
|
|
165
185
|
* stale non-approval at an earlier head never blocks and a later same-head 🟢
|
|
@@ -175,16 +195,28 @@ export function verifyFreshHumanApproval({ approvedBy, currentHeadSha, reviews =
|
|
|
175
195
|
* threads — the conductor's override is choosing to run the merge on a
|
|
176
196
|
* thread-clean 🔵.
|
|
177
197
|
* unrecognized disposition -> BLOCK (fail closed on a Copilot format change).
|
|
178
|
-
* 🟢 clean /
|
|
198
|
+
* 🟢 clean / headerless -> PASS.
|
|
199
|
+
*
|
|
200
|
+
* Three distinct states (`state` on the result):
|
|
201
|
+
* current_head_clean a current-head review passes (🟢 / headerless / 🔵);
|
|
202
|
+
* current_head_findings a current-head review blocks (🟡 / unrecognized);
|
|
203
|
+
* no_current_head_review no current-head Copilot review exists (none yet, or
|
|
204
|
+
* every review is on an earlier head). This state
|
|
205
|
+
* passes ONLY through a sanctioned disposition recorded
|
|
206
|
+
* for the current head (`absentReviewDisposition`
|
|
207
|
+
* `{ kind, headSha }`, kind in
|
|
208
|
+
* COPILOT_ABSENT_REVIEW_DISPOSITION). Absence alone never
|
|
209
|
+
* converges, and a stale earlier-head review is never
|
|
210
|
+
* inherited as the current-head verdict.
|
|
179
211
|
*
|
|
180
|
-
* @returns {{ ok: boolean, disposition: string|null, reason: string|null }}
|
|
212
|
+
* @returns {{ ok: boolean, state: string|null, disposition: string|null, reason: string|null }}
|
|
181
213
|
*/
|
|
182
|
-
export function evaluateCopilotConvergence({ currentHeadSha = null, reviews = [] } = {}) {
|
|
214
|
+
export function evaluateCopilotConvergence({ currentHeadSha = null, reviews = [], absentReviewDisposition = null } = {}) {
|
|
183
215
|
const head = typeof currentHeadSha === "string" ? currentHeadSha.trim() : "";
|
|
184
216
|
// Head unknown: no current-head review can be pinned. Fail closed (matches
|
|
185
217
|
// verifyFreshHumanApproval), so this precondition can never pass without a
|
|
186
218
|
// 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" };
|
|
219
|
+
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
220
|
|
|
189
221
|
// Mirror summarizeCopilotReviews' current-head finding selection BYTE-FOR-BYTE
|
|
190
222
|
// so the merge gate and the loop can never diverge (they already share
|
|
@@ -200,9 +232,17 @@ export function evaluateCopilotConvergence({ currentHeadSha = null, reviews = []
|
|
|
200
232
|
let latestDisposition = null;
|
|
201
233
|
let latestAt = null;
|
|
202
234
|
for (const entry of Array.isArray(reviews) ? reviews : []) {
|
|
203
|
-
|
|
235
|
+
// Shape-tolerant Copilot-login + commit extraction: the merge gate feeds
|
|
236
|
+
// REST-shaped reviews (user.login/commit_id) while the gate-ENTRY detector
|
|
237
|
+
// feeds `gh pr view` GraphQL-shaped reviews (author.login/commit.oid). Both
|
|
238
|
+
// call sites route through this one evaluateCopilotConvergence, so it must
|
|
239
|
+
// recognize BOTH shapes to produce ONE convergence verdict. Only Copilot
|
|
240
|
+
// reviews matter here, so broadening the login read to author.login cannot
|
|
241
|
+
// affect verifyFreshHumanApproval (which keeps its own REST-only
|
|
242
|
+
// reviewLogin/reviewCommit).
|
|
243
|
+
const login = copilotConvergenceReviewLogin(entry);
|
|
204
244
|
if (login === null || !isCopilotLogin(login)) continue;
|
|
205
|
-
if (
|
|
245
|
+
if (extractReviewCommitSha(entry) !== head) continue; // only current-head reviews
|
|
206
246
|
const state = typeof entry?.state === "string" ? entry.state.toUpperCase() : "";
|
|
207
247
|
if (state === "PENDING" || !SUBMITTED_REVIEW_STATES.has(state)) continue; // PENDING/unknown never sets the finding
|
|
208
248
|
const disposition = classifyCopilotReviewBodyDisposition(state, entry?.body);
|
|
@@ -219,16 +259,29 @@ export function evaluateCopilotConvergence({ currentHeadSha = null, reviews = []
|
|
|
219
259
|
}
|
|
220
260
|
// a null submittedAt once a non-null latest exists is ignored (mirrors summarize)
|
|
221
261
|
}
|
|
222
|
-
if (latestDisposition === null)
|
|
262
|
+
if (latestDisposition === null) {
|
|
263
|
+
const absent = COPILOT_CONVERGENCE_STATE.NO_CURRENT_HEAD_REVIEW;
|
|
264
|
+
const kind = absentReviewDisposition?.kind;
|
|
265
|
+
if (SANCTIONED_ABSENT_REVIEW_DISPOSITIONS.has(kind) && absentReviewDisposition?.headSha === head) {
|
|
266
|
+
return { ok: true, state: absent, disposition: kind, reason: null };
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
state: absent,
|
|
271
|
+
disposition: null,
|
|
272
|
+
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(", ")})`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
223
275
|
|
|
276
|
+
const findings = COPILOT_CONVERGENCE_STATE.CURRENT_HEAD_FINDINGS;
|
|
224
277
|
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` };
|
|
278
|
+
return { ok: false, state: findings, disposition: latestDisposition, reason: `current-head Copilot review is "Changes recommended" (🟡, actionable non-approval); converge to "Approval recommended" (🟢) or resolve the feedback before merge` };
|
|
226
279
|
}
|
|
227
280
|
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` };
|
|
281
|
+
return { ok: false, state: findings, disposition: latestDisposition, reason: `current-head Copilot review carries an unrecognized disposition header (fail closed); a recognized "Approval recommended" (🟢) is required` };
|
|
229
282
|
}
|
|
230
283
|
// CLEAN, NONE, and NEEDS_CLOSER_LOOK (🔵, conductor-overridable) pass.
|
|
231
|
-
return { ok: true, disposition: latestDisposition, reason: null };
|
|
284
|
+
return { ok: true, state: COPILOT_CONVERGENCE_STATE.CURRENT_HEAD_CLEAN, disposition: latestDisposition, reason: null };
|
|
232
285
|
}
|
|
233
286
|
|
|
234
287
|
// Disposition blocking precedence, most-blocking first. Used to fold an
|
|
@@ -327,6 +380,8 @@ export function evaluateMergePreconditions({
|
|
|
327
380
|
comments = [],
|
|
328
381
|
standingAuthorized = false,
|
|
329
382
|
stableRelease = false,
|
|
383
|
+
copilotAbsentReviewDisposition = null,
|
|
384
|
+
copilotBodyDisposition = null,
|
|
330
385
|
} = {}) {
|
|
331
386
|
const failures = [];
|
|
332
387
|
|
|
@@ -375,8 +430,15 @@ export function evaluateMergePreconditions({
|
|
|
375
430
|
|
|
376
431
|
// Copilot-convergence precondition: refuse a current-head Copilot non-approval
|
|
377
432
|
// body disposition, mirroring the loop's copilotBodyFeedbackUnresolved.
|
|
378
|
-
|
|
379
|
-
|
|
433
|
+
// A trusted copilot-body-disposition record resolved for the current head
|
|
434
|
+
// (`{ headSha, reviewId, ... }`) clears a current-head finding, as it does
|
|
435
|
+
// at gate entry.
|
|
436
|
+
const copilotConvergence = evaluateCopilotConvergence({ currentHeadSha, reviews, absentReviewDisposition: copilotAbsentReviewDisposition });
|
|
437
|
+
const head = typeof currentHeadSha === "string" ? currentHeadSha.trim().toLowerCase() : "";
|
|
438
|
+
const bodyCleared = copilotConvergence.state === COPILOT_CONVERGENCE_STATE.CURRENT_HEAD_FINDINGS
|
|
439
|
+
&& head.length > 0
|
|
440
|
+
&& typeof copilotBodyDisposition?.headSha === "string" && copilotBodyDisposition.headSha.toLowerCase() === head;
|
|
441
|
+
if (!copilotConvergence.ok && !bodyCleared) {
|
|
380
442
|
failures.push({ precondition: "copilot_convergence", reason: copilotConvergence.reason });
|
|
381
443
|
}
|
|
382
444
|
|
|
@@ -391,9 +453,12 @@ export function evaluateMergePreconditions({
|
|
|
391
453
|
failures,
|
|
392
454
|
mergeClass,
|
|
393
455
|
approvalVia: decision.authorized ? decision.via : null,
|
|
394
|
-
// Audit trace:
|
|
395
|
-
//
|
|
396
|
-
//
|
|
456
|
+
// Audit trace: which convergence state applied and what settled it (the
|
|
457
|
+
// current-head review disposition, or the sanctioned disposition kind when
|
|
458
|
+
// no current-head review exists), so a merge on a conductor-overridable 🔵
|
|
459
|
+
// or without a current-head review is recorded, never invisible.
|
|
460
|
+
copilotConvergenceState: copilotConvergence.state,
|
|
397
461
|
copilotDisposition: copilotConvergence.disposition,
|
|
462
|
+
copilotBodyDisposition: bodyCleared ? copilotBodyDisposition : null,
|
|
398
463
|
};
|
|
399
464
|
}
|