@dev-loops/core 1.0.1 → 1.0.2-slim.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/package.json +5 -2
- package/src/analysis/diff-analyzer.mjs +85 -137
- package/src/claude/asset-generation.mjs +7 -7
- package/src/claude/hook-decisions.mjs +167 -50
- package/src/config/config.mjs +388 -787
- package/src/config/extension-defaults.yaml +14 -9
- package/src/github/comment-id-guard.mjs +39 -1
- package/src/github/copilot-helpers.mjs +90 -158
- package/src/github/gh.mjs +49 -0
- package/src/loop/bash-command-classify.mjs +34 -49
- package/src/loop/commit-msg-guard.mjs +1 -1
- package/src/loop/conductor-routing.mjs +15 -23
- package/src/loop/copilot-loop-state.mjs +46 -94
- package/src/loop/gate-carry-forward.mjs +46 -22
- package/src/loop/gate-evidence-reconcile.mjs +75 -0
- package/src/loop/gate-fanin.mjs +266 -435
- package/src/loop/handoff-envelope.mjs +21 -21
- package/src/loop/issue-refinement-artifact.mjs +449 -284
- package/src/loop/lifecycle-state.mjs +10 -21
- package/src/loop/pr-gate-coordination.mjs +49 -49
- package/src/loop/queue-board-sync.mjs +16 -82
- package/src/loop/review-dispatch-plan.mjs +61 -122
- package/src/loop/review-lineage.mjs +19 -44
- package/src/loop/spec-authority.mjs +729 -0
- package/src/loop/steering.mjs +16 -68
- package/src/loop/ui-e2e-scoping.mjs +1 -0
- package/src/loop/worktree-guard.mjs +55 -0
- package/src/projects/list-queue-items.mjs +16 -175
- package/src/projects/move-queue-item.mjs +16 -171
- package/src/projects/projects-access.mjs +202 -0
- package/src/security/secret-scan.mjs +13 -1
|
@@ -1,30 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deterministic outer dev-loop lifecycle state model.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* This module provides:
|
|
10
|
-
* - LIFECYCLE_STATE: stable phase name constants
|
|
11
|
-
* - LIFECYCLE_TRANSITIONS: legal transition graph between phases
|
|
12
|
-
* - LIFECYCLE_GRAPH: metadata (start, end, entry, terminal, nonterminal)
|
|
13
|
-
* - LIFECYCLE_NEXT_ACTIONS: recommended next action for each phase
|
|
14
|
-
* - resolveLifecycleState: resolver that maps inputs to one lifecycle phase
|
|
15
|
-
* - getAllowedTransitions: helper to list allowed next phases
|
|
16
|
-
* - COPILOT_INNER_STATE_MAP: maps lifecycle phases to copilot-loop-state.mjs inner states
|
|
4
|
+
* Defines the sequential lifecycle phases — issue_intake → refinement →
|
|
5
|
+
* implementation → draft_gate → feedback_resolution → pre_approval_gate →
|
|
6
|
+
* merge — as a consultable graph so skills use machine-resolved state instead
|
|
7
|
+
* of restating the flow in prose.
|
|
17
8
|
*
|
|
18
9
|
* Contract guarantees:
|
|
19
|
-
* - One deterministic lifecycle phase per normalized input set
|
|
20
|
-
* - Ambiguous or incomplete inputs fall back to issue_intake
|
|
21
|
-
* -
|
|
22
|
-
* - Purely functional; no I/O or side effects
|
|
10
|
+
* - One deterministic lifecycle phase per normalized input set.
|
|
11
|
+
* - Ambiguous or incomplete inputs fall back to issue_intake.
|
|
12
|
+
* - Purely functional; no I/O or side effects.
|
|
23
13
|
*
|
|
24
|
-
* Integration boundary:
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
27
|
-
* - Lifecycle phases are the outer sequence; inner states are sub-phase detail
|
|
14
|
+
* Integration boundary: lifecycle phases are the outer sequence;
|
|
15
|
+
* copilot-loop-state.mjs remains the inner machine for the Copilot review
|
|
16
|
+
* portion (inner states are sub-phase detail).
|
|
28
17
|
*/
|
|
29
18
|
|
|
30
19
|
// ---------------------------------------------------------------------------
|
|
@@ -4,8 +4,8 @@ import { evaluateUiE2eScoping } from "./ui-e2e-scoping.mjs";
|
|
|
4
4
|
import { evaluateUiDesignerReviewScoping } from "./ui-designer-review-scoping.mjs";
|
|
5
5
|
import { trimmedOrNull } from "./normalize.mjs";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
MALFORMED_AC_DOD_MATRIX_FINDING,
|
|
8
|
+
MISSING_AC_DOD_MATRIX_FINDING,
|
|
9
9
|
MISSING_EXPLICIT_NON_GOALS_FINDING,
|
|
10
10
|
} from "./issue-refinement-artifact.mjs";
|
|
11
11
|
|
|
@@ -26,7 +26,7 @@ export const PR_CHECKPOINT = Object.freeze({
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Refinement-artifact gate check
|
|
29
|
+
* Refinement-artifact gate check.
|
|
30
30
|
*
|
|
31
31
|
* The draft gate must verify the linked issue has an explicit refinement
|
|
32
32
|
* artifact (Acceptance criteria / DoD / linked refinement doc) before it
|
|
@@ -246,16 +246,16 @@ function normalizeRefinementArtifactStatus(value) {
|
|
|
246
246
|
// their own validation-failure reason from the detector; that reason must
|
|
247
247
|
// replace the "linked issue" wording, which does not apply when the PR is the
|
|
248
248
|
// spec-of-record and no linked issue was ever expected.
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// gate's guidance (decideEnqueueRefinementGate) and the detector
|
|
249
|
+
// Matrix-floor vocabulary for the draft-gate blocked reason: which piece
|
|
250
|
+
// of the refinement floor is missing per finding — the SAME finding taxonomy
|
|
251
|
+
// the enqueue gate's guidance (decideEnqueueRefinementGate) and the detector
|
|
252
252
|
// (detectIssueRefinementArtifact) use, so the draft gate (the unconditional
|
|
253
|
-
// backstop for that floor) cannot drift from it.
|
|
254
|
-
// linked issue
|
|
255
|
-
// the
|
|
253
|
+
// backstop for that floor) cannot drift from it. A checklist-only or
|
|
254
|
+
// matrix-malformed linked issue names the actually-missing/invalid artifact so
|
|
255
|
+
// the fix is not misdirected.
|
|
256
256
|
const REFINEMENT_MISSING_ARM_BY_FINDING = Object.freeze({
|
|
257
|
-
[
|
|
258
|
-
[
|
|
257
|
+
[MISSING_AC_DOD_MATRIX_FINDING]: "an AC→DoD mapping matrix (a two-column table mapping each acceptance-criterion outcome to its required completion evidence)",
|
|
258
|
+
[MALFORMED_AC_DOD_MATRIX_FINDING]: "a valid AC→DoD mapping matrix (the existing table is empty or identifier-only/tautological)",
|
|
259
259
|
[MISSING_EXPLICIT_NON_GOALS_FINDING]: "an explicit Non-goals section",
|
|
260
260
|
});
|
|
261
261
|
|
|
@@ -269,15 +269,15 @@ function formatRefinementBlockedReason(linkedIssue, status, refinementArtifact)
|
|
|
269
269
|
: REFINEMENT_ARTIFACT_FINDING;
|
|
270
270
|
const missingArm = REFINEMENT_MISSING_ARM_BY_FINDING[finding];
|
|
271
271
|
if (missingArm !== undefined) {
|
|
272
|
-
return `Linked issue #${linkedIssue} has an incomplete refinement matrix — it is missing ${missingArm}. Add it to the issue body to complete the
|
|
272
|
+
return `Linked issue #${linkedIssue} has an incomplete refinement matrix — it is missing ${missingArm}. Add it to the issue body to complete the AC→DoD mapping matrix + Non-goals refinement floor, then re-open the draft PR. finding=${finding}`;
|
|
273
273
|
}
|
|
274
274
|
if (linkedIssue !== null && Number.isInteger(linkedIssue)) {
|
|
275
|
-
return `Linked issue #${linkedIssue} has no refinement artifact (no
|
|
275
|
+
return `Linked issue #${linkedIssue} has no refinement artifact (no AC→DoD mapping matrix, or a resolvable linked refinement doc). Refine the issue to the authoritative AC→DoD mapping matrix plus an explicit Non-goals section — or link a refinement doc (tmp/refinement/*.md), a complete artifact on its own — then re-open the draft PR. finding=${REFINEMENT_ARTIFACT_FINDING}`;
|
|
276
276
|
}
|
|
277
|
-
return `The draft gate cannot complete: the linked issue has no detectable refinement artifact (no
|
|
277
|
+
return `The draft gate cannot complete: the linked issue has no detectable refinement artifact (no AC→DoD mapping matrix, or a resolvable linked refinement doc). finding=${REFINEMENT_ARTIFACT_FINDING}`;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
//
|
|
280
|
+
// Describes the CI state a round-cap-reached fallback branch actually
|
|
281
281
|
// granted on, for human-read reason text. preApprovalRequireCi:false plus a
|
|
282
282
|
// non-success/non-crediblyGreen ciStatus is not "green CI" — claiming it is
|
|
283
283
|
// would be the same false-CI-claim buildRoundExhaustionGateEvidenceNote below
|
|
@@ -301,7 +301,7 @@ function buildRoundExhaustionGateEvidenceNote({ copilotReviewRoundCount, maxCopi
|
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
303
|
* Blocked result for a PR that would otherwise reach final_approval_ready but
|
|
304
|
-
* still carries a merge-blocking marker in its title
|
|
304
|
+
* still carries a merge-blocking marker in its title. The title is
|
|
305
305
|
* the most visible contract surface, so a WIP/DRAFT/DO NOT MERGE title must
|
|
306
306
|
* block the final-approval boundary just like the mark-ready transition does.
|
|
307
307
|
*/
|
|
@@ -445,7 +445,7 @@ function buildResult({
|
|
|
445
445
|
* caller must run request-copilot-review.mjs first.
|
|
446
446
|
*
|
|
447
447
|
* Exception: round-cap clean fallback (rounds exhausted + clean converged)
|
|
448
|
-
* does not require a formal re-request
|
|
448
|
+
* does not require a formal re-request.
|
|
449
449
|
*
|
|
450
450
|
* @param {object} params
|
|
451
451
|
* @param {string} params.copilotReviewRequestStatus - "none"|"requested"|"already-requested"|"unavailable"|"failed"
|
|
@@ -454,10 +454,10 @@ function buildResult({
|
|
|
454
454
|
* @param {number|null} params.maxCopilotRounds
|
|
455
455
|
* @param {boolean} params.sameHeadCleanConverged
|
|
456
456
|
* @param {boolean} [params.roundCapCleanFallback=false] - interpreter resolved the
|
|
457
|
-
* round-cap clean fallback
|
|
457
|
+
* round-cap clean fallback: rounds exhausted + clean threads + green CI on
|
|
458
458
|
* the current head, including a post-cap head Copilot has not (and will not)
|
|
459
459
|
* re-review. No further Copilot round is permitted, so the formal-request guard
|
|
460
|
-
* must not fire — the pre_approval_gate reviews the post-cap head
|
|
460
|
+
* must not fire — the pre_approval_gate reviews the post-cap head.
|
|
461
461
|
* @param {boolean} [params.postConvergenceSignificantChange=false] - significant
|
|
462
462
|
* post-convergence changes on a newer head start a new review cycle and must
|
|
463
463
|
* not be treated as round-cap clean-fallback suppression.
|
|
@@ -493,17 +493,17 @@ export function shouldGuardCopilotReviewRequest({
|
|
|
493
493
|
}
|
|
494
494
|
// Durable signal: if Copilot was ever formally requested as a reviewer,
|
|
495
495
|
// the current "none" status is from a fulfilled request (normal cycle),
|
|
496
|
-
// not from a missing request. Do not guard the happy path
|
|
496
|
+
// not from a missing request. Do not guard the happy path.
|
|
497
497
|
if (copilotReviewEverFormallyRequested) {
|
|
498
498
|
return false;
|
|
499
499
|
}
|
|
500
500
|
// Round-cap clean fallback: exhausted rounds + clean converged does not require
|
|
501
501
|
// a formal re-request. This covers two shapes of "clean at the cap":
|
|
502
502
|
// - sameHeadCleanConverged: the current head itself carries a clean Copilot review;
|
|
503
|
-
// - roundCapCleanFallback
|
|
503
|
+
// - roundCapCleanFallback: the head is clean (zero unresolved threads + green
|
|
504
504
|
// CI) but Copilot has NOT reviewed THIS head (e.g. a post-cap commit). No further
|
|
505
505
|
// Copilot round is permitted, so forcing a formal request would dead-end the loop;
|
|
506
|
-
// the pre_approval_gate reviews the post-cap head instead
|
|
506
|
+
// the pre_approval_gate reviews the post-cap head instead.
|
|
507
507
|
const roundCapReached = isCopilotRoundCapReached({ copilotReviewRoundCount, maxCopilotRounds });
|
|
508
508
|
if (
|
|
509
509
|
roundCapReached
|
|
@@ -517,7 +517,7 @@ export function shouldGuardCopilotReviewRequest({
|
|
|
517
517
|
|
|
518
518
|
/**
|
|
519
519
|
* Boundaries at which a non-draft PR must NOT carry a merge-blocking title
|
|
520
|
-
* marker
|
|
520
|
+
* marker. A WIP/DRAFT/DO NOT MERGE/🚧 title is acceptable
|
|
521
521
|
* while the PR is still in draft, but the moment the PR leaves draft and reaches
|
|
522
522
|
* the pre-approval gate boundary (entry) or the final-approval boundary, the
|
|
523
523
|
* title is a live merge-contract surface and must be clean. The guard is applied
|
|
@@ -532,7 +532,7 @@ const TITLE_MARKER_GUARDED_BOUNDARIES = Object.freeze([
|
|
|
532
532
|
]);
|
|
533
533
|
|
|
534
534
|
/**
|
|
535
|
-
* Independent gate-ENTRY re-check
|
|
535
|
+
* Independent gate-ENTRY re-check: even when the caller's
|
|
536
536
|
* lifecycleState/sameHeadCleanConverged claims a settled Copilot convergence,
|
|
537
537
|
* an outstanding (`requested`/`already-requested`) Copilot review request on
|
|
538
538
|
* the CURRENT head is a second, independent "unsettled" signal — not derived
|
|
@@ -547,8 +547,8 @@ const TITLE_MARKER_GUARDED_BOUNDARIES = Object.freeze([
|
|
|
547
547
|
* already been spent.
|
|
548
548
|
*
|
|
549
549
|
* Skipped when Copilot review is not required at all — `reviewMode:
|
|
550
|
-
* "internal_only"` or `maxCopilotRounds: 0` — preserving the existing
|
|
551
|
-
*
|
|
550
|
+
* "internal_only"` or `maxCopilotRounds: 0` — preserving the existing
|
|
551
|
+
* exemptions (internal-only and light-dispatched-with-disabled-review
|
|
552
552
|
* PRs never need a Copilot round in the first place).
|
|
553
553
|
*/
|
|
554
554
|
const PRE_APPROVAL_ENTRY_BOUNDARIES = Object.freeze([
|
|
@@ -589,7 +589,7 @@ function applyUnsettledCopilotReviewEntryGuard(input, result) {
|
|
|
589
589
|
if (copilotReviewRequestStatus !== "requested" && copilotReviewRequestStatus !== "already-requested") {
|
|
590
590
|
return null;
|
|
591
591
|
}
|
|
592
|
-
// Round-cap exemption (mirrors shouldGuardCopilotReviewRequest
|
|
592
|
+
// Round-cap exemption (mirrors shouldGuardCopilotReviewRequest):
|
|
593
593
|
// past the cap a lingering requested/already-requested status is for a review
|
|
594
594
|
// that can never come (no further round is permitted), so treating it as
|
|
595
595
|
// "unsettled" here would re-introduce the infinite-wait dead-end the
|
|
@@ -602,7 +602,7 @@ function applyUnsettledCopilotReviewEntryGuard(input, result) {
|
|
|
602
602
|
maxCopilotRounds: input.maxCopilotRounds,
|
|
603
603
|
});
|
|
604
604
|
const lifecycleState = typeof input.lifecycleState === "string" ? input.lifecycleState.trim().toLowerCase() : "";
|
|
605
|
-
// Also exempt the evaluator's own ROUND_CAP_REACHED grant shape
|
|
605
|
+
// Also exempt the evaluator's own ROUND_CAP_REACHED grant shape:
|
|
606
606
|
// without this, this guard would rewrite that grant back to
|
|
607
607
|
// waiting_for_copilot_review the instant it is produced, re-introducing the
|
|
608
608
|
// never-arriving-review dead-end the round-cap exemption exists to prevent.
|
|
@@ -657,7 +657,7 @@ function applyUnsettledCopilotReviewEntryGuard(input, result) {
|
|
|
657
657
|
|
|
658
658
|
/**
|
|
659
659
|
* Evaluates PR gate coordination, then re-asserts the merge-blocking title guard
|
|
660
|
-
*
|
|
660
|
+
* at the pre-approval / final-approval boundary for non-draft PRs.
|
|
661
661
|
*
|
|
662
662
|
* The title check is also performed inline at the three FINAL_APPROVAL_READY
|
|
663
663
|
* sites (defense in depth); this wrapper additionally covers the pre-approval
|
|
@@ -708,7 +708,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
708
708
|
const prClosed = input.prClosed === true;
|
|
709
709
|
const prMerged = input.prMerged === true;
|
|
710
710
|
const sameHeadCleanConverged = input.sameHeadCleanConverged === true;
|
|
711
|
-
// Operator-authorized post-convergence suppression
|
|
711
|
+
// Operator-authorized post-convergence suppression: set only when the
|
|
712
712
|
// caller has verified an explicit prior withdrawal (withdraw-copilot-review-
|
|
713
713
|
// request.mjs) recorded a suppression marker for this EXACT head, proving the
|
|
714
714
|
// delta since Copilot's last submitted review is a pure doc/prose bump. Never
|
|
@@ -737,7 +737,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
737
737
|
const copilotReviewRoundCount = normalizeNonNegativeInteger(input.copilotReviewRoundCount);
|
|
738
738
|
const maxCopilotRounds = normalizePositiveInteger(input.maxCopilotRounds);
|
|
739
739
|
const roundCapReached = isCopilotRoundCapReached({ copilotReviewRoundCount, maxCopilotRounds });
|
|
740
|
-
//
|
|
740
|
+
// Explicit current-head unresolved-thread signal for the round-cap
|
|
741
741
|
// fallback check below (STATE.ROUND_CAP_REACHED). Unlike copilotReviewRoundCount
|
|
742
742
|
// (which safely defaults to 0 when absent), an absent/invalid count here must
|
|
743
743
|
// NOT be coerced to 0 — that would silently treat an unknown thread count as
|
|
@@ -749,11 +749,11 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
749
749
|
const postConvergenceSignificantChange = input.postConvergenceSignificantChange === true;
|
|
750
750
|
const roundCapNewCycleRequired = roundCapReached && copilotReviewRoundCount > 0 && postConvergenceSignificantChange;
|
|
751
751
|
const prTitle = typeof input.prTitle === "string" ? input.prTitle : "";
|
|
752
|
-
// UI e2e auto-scoping
|
|
752
|
+
// UI e2e auto-scoping: the PR changed-file set + whether the shared UI
|
|
753
753
|
// e2e suite passed for this head. Inclusion is path-triggered, never annotated.
|
|
754
754
|
const changedFiles = Array.isArray(input.changedFiles) ? input.changedFiles : [];
|
|
755
755
|
const uiE2ePassed = input.uiE2ePassed === true ? true : (input.uiE2ePassed === false ? false : null);
|
|
756
|
-
// Designer/vision recorded-evidence scoping (
|
|
756
|
+
// Designer/vision recorded-evidence scoping (ADR 0041 UI half). See
|
|
757
757
|
// the designer-review scoping block below. Evidence reuses the loop's
|
|
758
758
|
// existing outcome + artifact-bundle record; exempt when a light/spike
|
|
759
759
|
// relaxed-gate carve-out applies.
|
|
@@ -834,7 +834,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
834
834
|
});
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
-
// Mergeability is a required precondition at every gate
|
|
837
|
+
// Mergeability is a required precondition at every gate. GitHub
|
|
838
838
|
// computes `mergeable` asynchronously, so an unsettled UNKNOWN must fail closed
|
|
839
839
|
// to a recheck — never a pass. The detect layer already re-polls a bounded
|
|
840
840
|
// number of times; if it still reads UNKNOWN here, hold gate progression and
|
|
@@ -908,13 +908,13 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
908
908
|
});
|
|
909
909
|
}
|
|
910
910
|
|
|
911
|
-
// UI e2e auto-scoping precondition
|
|
911
|
+
// UI e2e auto-scoping precondition. Path-triggered + fail-closed:
|
|
912
912
|
// if the PR's changed files touch a rendered artifact (a deck under
|
|
913
913
|
// docs/articles|presentations, or the inspect-run viewer source), it MUST be
|
|
914
914
|
// registered in the shared UI e2e suite AND that suite must have passed for
|
|
915
915
|
// this head. A rendered-artifact change with no registered/passing coverage
|
|
916
916
|
// blocks here with a reason naming the artifact. Distinct seam from the
|
|
917
|
-
// mergeability
|
|
917
|
+
// mergeability preconditions to minimize
|
|
918
918
|
// merge-time conflict. Non-UI changes pass through untouched (required=false).
|
|
919
919
|
const uiE2eScoping = evaluateUiE2eScoping(changedFiles, { uiE2ePassed });
|
|
920
920
|
if (uiE2eScoping.required && !uiE2eScoping.satisfied) {
|
|
@@ -947,7 +947,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
947
947
|
});
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
-
// Designer/vision recorded-evidence precondition (
|
|
950
|
+
// Designer/vision recorded-evidence precondition (ADR 0041 UI half).
|
|
951
951
|
// Path-triggered + fail-closed, modeled on the UI e2e scoping block above: if
|
|
952
952
|
// the PR's changed files touch a rendered artifact (docs/articles|presentations
|
|
953
953
|
// HTML), it MUST carry recorded designer/vision review evidence (the loop's
|
|
@@ -1389,7 +1389,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1389
1389
|
// straight to pre_approval_gate, so this branch must not force a re-request
|
|
1390
1390
|
// for a missing Copilot convergence point that will never exist. The
|
|
1391
1391
|
// sibling PR_READY_NO_FEEDBACK branch already honors internal_only; this
|
|
1392
|
-
// reconciles READY_TO_REREQUEST_REVIEW with it
|
|
1392
|
+
// reconciles READY_TO_REREQUEST_REVIEW with it. Non-suppressed
|
|
1393
1393
|
// external-review PRs keep reviewMode null and hit the guard unchanged.
|
|
1394
1394
|
if (!sameHeadCleanConverged && !postConvergenceReviewSuppressed && reviewMode !== "internal_only" && (!roundCapReached || roundCapNewCycleRequired)) {
|
|
1395
1395
|
pushUnique(allowedNextActions, [PR_CHECKPOINT_ACTION.REREQUEST_COPILOT_REVIEW]);
|
|
@@ -1511,17 +1511,17 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1511
1511
|
});
|
|
1512
1512
|
}
|
|
1513
1513
|
|
|
1514
|
-
// Round-cap clean fallback
|
|
1514
|
+
// Round-cap clean fallback: the Copilot review round cap is
|
|
1515
1515
|
// exhausted and the current head is clean (zero unresolved threads + green CI)
|
|
1516
1516
|
// — including a POST-CAP head Copilot has not (and will not) re-review, since
|
|
1517
1517
|
// no further Copilot round is permitted. Re-requesting review is illegal here,
|
|
1518
1518
|
// so this MUST NOT dead-end at READY_TO_REREQUEST_REVIEW — nor at a forced
|
|
1519
|
-
// rerequest for a post-convergence significant change
|
|
1519
|
+
// rerequest for a post-convergence significant change: the cap makes
|
|
1520
1520
|
// that rerequest impossible (request-copilot-review suppresses it), so a
|
|
1521
1521
|
// significant change discovered here is reviewed by the pre_approval_gate
|
|
1522
1522
|
// fan-out itself, on the post-cap head, same as any other clean fallback. It
|
|
1523
1523
|
// routes to the pre_approval_gate, which reviews the post-cap head itself
|
|
1524
|
-
//
|
|
1524
|
+
// itself. The CI guards below still hold (failing / credibly-green CI
|
|
1525
1525
|
// blocks), and conflicts / blocked states are handled earlier, so
|
|
1526
1526
|
// genuinely-blocked states still forbid pre_approval. Mirrors
|
|
1527
1527
|
// LOW_SIGNAL_CONVERGED routing with round-cap reasoning.
|
|
@@ -1587,10 +1587,10 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1587
1587
|
refinementArtifact,
|
|
1588
1588
|
});
|
|
1589
1589
|
}
|
|
1590
|
-
// Mirror LOW_SIGNAL_CONVERGED
|
|
1590
|
+
// Mirror LOW_SIGNAL_CONVERGED: a clean current head with no clean
|
|
1591
1591
|
// draft_gate evidence must reconcile the draft gate rather than jump to
|
|
1592
1592
|
// final approval. This keeps the core handler consistent with the
|
|
1593
|
-
// detect-pr-gate-coordination-state
|
|
1593
|
+
// detect-pr-gate-coordination-state post-pass, which unconditionally
|
|
1594
1594
|
// downgrades FINAL_APPROVAL_READY → DRAFT_GATE_NEEDED when
|
|
1595
1595
|
// draftGate.cleanEvidenceExists is false (no ROUND_CAP_CLEAN_FALLBACK
|
|
1596
1596
|
// exemption). Without this guard the final-approval-without-draft-gate
|
|
@@ -1668,23 +1668,23 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1668
1668
|
});
|
|
1669
1669
|
}
|
|
1670
1670
|
|
|
1671
|
-
// Defensive gate-entry re-check for ROUND_CAP_REACHED
|
|
1671
|
+
// Defensive gate-entry re-check for ROUND_CAP_REACHED: the compound
|
|
1672
1672
|
// "unresolved threads OR non-clean CI" hard stop that copilot-loop-state.mjs
|
|
1673
1673
|
// emits has no dedicated boundary of its own, so without this branch the
|
|
1674
1674
|
// generic fallback below always names `report_blocked`. Both shipped
|
|
1675
1675
|
// callers read lifecycleState and the CI/thread facts from ONE snapshot, and
|
|
1676
1676
|
// the interpreter's own CI predicate is strictly wider than this branch's
|
|
1677
|
-
// (it also accepts `crediblyGreen
|
|
1677
|
+
// (it also accepts `crediblyGreen`): whenever this branch's narrower
|
|
1678
1678
|
// predicate and zero unresolved threads hold, the interpreter has already
|
|
1679
1679
|
// classified the snapshot ROUND_CAP_CLEAN_FALLBACK, never ROUND_CAP_REACHED
|
|
1680
1680
|
// (see the equivalence test in pr-gate-coordination.test.mjs). This branch
|
|
1681
1681
|
// is therefore unreachable through the shipped callers — it is pure
|
|
1682
|
-
// defense-in-depth, demanded by
|
|
1682
|
+
// defense-in-depth, demanded by the corrected AC so the three
|
|
1683
1683
|
// fields can never disagree if a future caller hands the evaluator a
|
|
1684
1684
|
// round_cap_reached label alongside facts that satisfy the grant. The
|
|
1685
1685
|
// predicate intentionally mirrors every other pre-approval CI boundary in
|
|
1686
1686
|
// this file (success, or CI not required) — `crediblyGreen` is unconfirmed
|
|
1687
|
-
// CI and stays blocked here exactly as it does everywhere else
|
|
1687
|
+
// CI and stays blocked here exactly as it does everywhere else. Any
|
|
1688
1688
|
// other combination (threads still unresolved, an unknown thread count, or
|
|
1689
1689
|
// CI not confirmed green) falls through unchanged to the generic default
|
|
1690
1690
|
// below, preserving today's blocked behavior exactly.
|
|
@@ -1694,7 +1694,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1694
1694
|
// OR CI is not required by config — never for an unconfirmed/failing/absent
|
|
1695
1695
|
// status gated in only because requireCi is false. Naming it "green" in
|
|
1696
1696
|
// human-read reason/evidence text for the latter case would be a false CI
|
|
1697
|
-
// claim
|
|
1697
|
+
// claim, so describe the actual grant basis instead.
|
|
1698
1698
|
const ciClause = ciStatus === "success" ? "green CI" : "CI not required by config";
|
|
1699
1699
|
if (unresolvedThreadCount === 0 && ciConfirmedGreen) {
|
|
1700
1700
|
if (preApprovalGate.currentHeadClean) {
|
|
@@ -1717,7 +1717,7 @@ function evaluatePrGateCoordinationCore(input = {}) {
|
|
|
1717
1717
|
refinementArtifact,
|
|
1718
1718
|
});
|
|
1719
1719
|
}
|
|
1720
|
-
// Mirror ROUND_CAP_CLEAN_FALLBACK
|
|
1720
|
+
// Mirror ROUND_CAP_CLEAN_FALLBACK: a clean current head with no clean
|
|
1721
1721
|
// draft_gate evidence must reconcile the draft gate rather than jump to
|
|
1722
1722
|
// final approval.
|
|
1723
1723
|
if (!draftGate.cleanEvidenceExists) {
|
|
@@ -2,7 +2,8 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { main as moveQueueItemMain } from "../projects/move-queue-item.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveOwner } from "../github/gh.mjs";
|
|
6
|
+
import { discoverProjects } from "../projects/projects-access.mjs";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_NON_SUCCESS_COLUMN = "Backlog";
|
|
8
9
|
|
|
@@ -40,7 +41,7 @@ export const DEFAULT_STATE_COLUMN_NAMES = Object.freeze({
|
|
|
40
41
|
/**
|
|
41
42
|
* Default loop-state → logical-column map. Covers both the lifecycle states
|
|
42
43
|
* (lifecycle-state.mjs) and the inner Copilot loop states (copilot-loop-state.mjs),
|
|
43
|
-
* plus the conceptual names
|
|
44
|
+
* plus the conceptual state names. Unknown states fall back to
|
|
44
45
|
* IN_PROGRESS (a safe, visible "work is happening" column) rather than throwing.
|
|
45
46
|
*/
|
|
46
47
|
export const DEFAULT_STATE_LOGICAL_MAP = Object.freeze({
|
|
@@ -53,7 +54,7 @@ export const DEFAULT_STATE_LOGICAL_MAP = Object.freeze({
|
|
|
53
54
|
|
|
54
55
|
// In Progress — active implementation / review / feedback resolution
|
|
55
56
|
implementation: LOGICAL_COLUMN.IN_PROGRESS,
|
|
56
|
-
// Tolerated alias for `implementation` (conceptual name
|
|
57
|
+
// Tolerated alias for `implementation` (conceptual name);
|
|
57
58
|
// the queue driver passes the real `implementation` lifecycle state.
|
|
58
59
|
local_implementation_active: LOGICAL_COLUMN.IN_PROGRESS,
|
|
59
60
|
draft_gate: LOGICAL_COLUMN.IN_PROGRESS,
|
|
@@ -80,7 +81,7 @@ export const DEFAULT_STATE_LOGICAL_MAP = Object.freeze({
|
|
|
80
81
|
// Done — terminal (lifecycle MERGE = "merge", queue terminal = "done")
|
|
81
82
|
merge: LOGICAL_COLUMN.DONE,
|
|
82
83
|
done: LOGICAL_COLUMN.DONE,
|
|
83
|
-
// Tolerated aliases (conceptual names
|
|
84
|
+
// Tolerated aliases (conceptual names).
|
|
84
85
|
merged: LOGICAL_COLUMN.DONE,
|
|
85
86
|
issue_closed: LOGICAL_COLUMN.DONE,
|
|
86
87
|
});
|
|
@@ -109,7 +110,7 @@ export function boardColumnForLoopState(loopState, mapping = {}) {
|
|
|
109
110
|
|
|
110
111
|
/**
|
|
111
112
|
* Derive the board's target LOGICAL column for a queue item from live GitHub
|
|
112
|
-
* facts
|
|
113
|
+
* facts. Returns LOGICAL_COLUMN.DONE, LOGICAL_COLUMN.IN_PROGRESS, or
|
|
113
114
|
* null when the item should be left where it is (Backlog/Next Up untouched).
|
|
114
115
|
*
|
|
115
116
|
* facts: {
|
|
@@ -131,7 +132,7 @@ export function deriveReconcileColumn(facts = {}) {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
/**
|
|
134
|
-
* Pure reconcile planner
|
|
135
|
+
* Pure reconcile planner. Given listed board items, a map of live facts
|
|
135
136
|
* keyed by the item's stable GraphQL node id (`item.itemId`), and the resolved
|
|
136
137
|
* column display names, return the set of moves needed to converge the board and
|
|
137
138
|
* a count of items left unchanged. Idempotent: when every item already sits in
|
|
@@ -139,7 +140,7 @@ export function deriveReconcileColumn(facts = {}) {
|
|
|
139
140
|
*
|
|
140
141
|
* Keying by the stable `itemId` (not the bare issue/PR number) keeps reconcile
|
|
141
142
|
* deterministic on a multi-repo GitHub Projects board, where two items can share
|
|
142
|
-
* a number (repo-A PR
|
|
143
|
+
* a number (repo-A PR 5 vs repo-B issue 5) — number-keying would collide and
|
|
143
144
|
* make moves order-dependent.
|
|
144
145
|
*
|
|
145
146
|
* items: [{ itemId, issueNumber, prNumber, status, ... }] (from list-queue-items)
|
|
@@ -172,7 +173,7 @@ function readDevloopsSettings(repoRoot) {
|
|
|
172
173
|
try {
|
|
173
174
|
const raw = readFileSync(base + ext, "utf8");
|
|
174
175
|
const settings = ext === ".json" ? JSON.parse(raw) : parseYaml(raw);
|
|
175
|
-
// `tracker` (
|
|
176
|
+
// `tracker` (the tracker-agnostic seam) is surfaced so
|
|
176
177
|
// loadBoardConfig can read tracker.board directly.
|
|
177
178
|
return { settings: settings?.queue ?? null, tracker: settings?.tracker ?? null };
|
|
178
179
|
} catch (err) {
|
|
@@ -207,7 +208,7 @@ export function loadBoardConfig(repoRoot) {
|
|
|
207
208
|
if (error) {
|
|
208
209
|
return { enabled: false, reason: `config read/parse error: ${error}` };
|
|
209
210
|
}
|
|
210
|
-
// tracker.board (canonical board key
|
|
211
|
+
// tracker.board (canonical board key) — see resolveTrackerBoard
|
|
211
212
|
// in ../config/config.mjs for the equivalent resolution against the
|
|
212
213
|
// validated, loaded config.
|
|
213
214
|
const trackerBoard = boardSelector(tracker?.board);
|
|
@@ -270,86 +271,19 @@ export function loadStateColumnMap(repoRoot) {
|
|
|
270
271
|
|
|
271
272
|
// Surface a non-ENOENT read/parse error (mirrors loadBoardConfig). Callers on
|
|
272
273
|
// the fail-closed next_up pickup path MUST honor it rather than silently
|
|
273
|
-
// querying the default literal column against a stale/renamed board
|
|
274
|
+
// querying the default literal column against a stale/renamed board.
|
|
274
275
|
// Existing `.columnNames`-only callers ignore this field and behave unchanged.
|
|
275
276
|
return { columnNames, stateColumnMap, error: error ?? null };
|
|
276
277
|
}
|
|
277
278
|
|
|
278
279
|
// ── Minimal project lookup (read-only, no create/repair) ────────────────
|
|
279
280
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"}"
|
|
284
|
-
].join("\n");
|
|
285
|
-
|
|
286
|
-
const GET_ORG_ID = [
|
|
287
|
-
"query($login:String!) {",
|
|
288
|
-
" organization(login:$login) { id }",
|
|
289
|
-
"}"
|
|
290
|
-
].join("\n");
|
|
291
|
-
|
|
292
|
-
const LIST_USER_PROJECTS = [
|
|
293
|
-
"query($login:String!, $after:String) {",
|
|
294
|
-
" user(login:$login) {",
|
|
295
|
-
" projectsV2(first:50, after:$after) {",
|
|
296
|
-
" pageInfo { hasNextPage endCursor }",
|
|
297
|
-
" nodes { id number title url }",
|
|
298
|
-
" }",
|
|
299
|
-
" }",
|
|
300
|
-
"}"
|
|
301
|
-
].join("\n");
|
|
302
|
-
|
|
303
|
-
const LIST_ORG_PROJECTS = [
|
|
304
|
-
"query($login:String!, $after:String) {",
|
|
305
|
-
" organization(login:$login) {",
|
|
306
|
-
" projectsV2(first:50, after:$after) {",
|
|
307
|
-
" pageInfo { hasNextPage endCursor }",
|
|
308
|
-
" nodes { id number title url }",
|
|
309
|
-
" }",
|
|
310
|
-
" }",
|
|
311
|
-
"}"
|
|
312
|
-
].join("\n");
|
|
313
|
-
|
|
314
|
-
async function resolveOwner(login, env, runChild) {
|
|
315
|
-
const userPayload = await ghGraphql(GET_USER_ID, { login }, env, runChild);
|
|
316
|
-
if (userPayload?.data?.user?.id) {
|
|
317
|
-
return { id: userPayload.data.user.id, kind: "user" };
|
|
318
|
-
}
|
|
319
|
-
const orgPayload = await ghGraphql(GET_ORG_ID, { login }, env, runChild);
|
|
320
|
-
if (orgPayload?.data?.organization?.id) {
|
|
321
|
-
return { id: orgPayload.data.organization.id, kind: "org" };
|
|
322
|
-
}
|
|
323
|
-
throw Object.assign(
|
|
324
|
-
new Error(`Could not resolve owner ID for "${login}"`),
|
|
325
|
-
{ code: "NO_USER_ID" },
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
281
|
+
// Board-sync tolerates null project nodes (defensive against a partial page);
|
|
282
|
+
// the shared discovery returns raw nodes, so this caller keeps its own null
|
|
283
|
+
// filter at the call site rather than in the shared traversal.
|
|
329
284
|
async function listAllProjects(login, kind, env, runChild) {
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
let after = null;
|
|
333
|
-
while (true) {
|
|
334
|
-
const vars = { login };
|
|
335
|
-
if (after) vars.after = after;
|
|
336
|
-
const payload = await ghGraphql(query, vars, env, runChild);
|
|
337
|
-
const connection = kind === "org"
|
|
338
|
-
? payload?.data?.organization?.projectsV2
|
|
339
|
-
: payload?.data?.user?.projectsV2;
|
|
340
|
-
const nodes = connection?.nodes ?? [];
|
|
341
|
-
projects.push(...nodes.filter((n) => n != null));
|
|
342
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
343
|
-
if (!pageInfo.hasNextPage) break;
|
|
344
|
-
if (!pageInfo.endCursor) {
|
|
345
|
-
throw Object.assign(
|
|
346
|
-
new Error("Invalid projects list payload: hasNextPage is true but endCursor is missing"),
|
|
347
|
-
{ code: "GH_API_ERROR" },
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
after = pageInfo.endCursor;
|
|
351
|
-
}
|
|
352
|
-
return projects;
|
|
285
|
+
const projects = await discoverProjects(login, kind, env, runChild);
|
|
286
|
+
return projects.filter((n) => n != null);
|
|
353
287
|
}
|
|
354
288
|
|
|
355
289
|
const projectNumberCache = new Map();
|