@dev-loops/core 1.0.2-pre.0 → 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 +2 -1
- package/src/analysis/diff-analyzer.mjs +85 -137
- package/src/claude/asset-generation.mjs +7 -7
- package/src/claude/hook-decisions.mjs +29 -36
- package/src/config/config.mjs +388 -787
- package/src/github/comment-id-guard.mjs +2 -2
- package/src/github/copilot-helpers.mjs +90 -158
- package/src/loop/bash-command-classify.mjs +34 -49
- package/src/loop/conductor-routing.mjs +15 -23
- package/src/loop/copilot-loop-state.mjs +46 -94
- package/src/loop/gate-carry-forward.mjs +2 -2
- package/src/loop/gate-fanin.mjs +252 -442
- package/src/loop/handoff-envelope.mjs +19 -19
- package/src/loop/issue-refinement-artifact.mjs +158 -252
- package/src/loop/lifecycle-state.mjs +10 -21
- package/src/loop/pr-gate-coordination.mjs +37 -37
- package/src/loop/queue-board-sync.mjs +16 -55
- package/src/loop/review-dispatch-plan.mjs +60 -122
- package/src/loop/review-lineage.mjs +19 -44
- package/src/loop/spec-authority.mjs +39 -69
- package/src/loop/steering.mjs +16 -68
- package/src/projects/list-queue-items.mjs +16 -146
- package/src/projects/move-queue-item.mjs +15 -141
- package/src/projects/projects-access.mjs +202 -0
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Conductor routing contract: deterministic routing and handoff decisions
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* This module provides:
|
|
6
|
-
* - ROUTING_OUTCOME: closed routing outcome taxonomy constants
|
|
7
|
-
* - LOOP_FAMILY: loop family identifier constants
|
|
8
|
-
* - SOURCE_MODE: confidence/source mode constants
|
|
9
|
-
* - ENTRYPOINT: handoff entrypoint identifier constants
|
|
10
|
-
* - STOP_REASON: stop reason code constants (for outer-loop backward compat)
|
|
11
|
-
* - evaluateConductorRouting: shared evaluator/policy entrypoint
|
|
2
|
+
* Conductor routing contract: deterministic routing and handoff decisions above
|
|
3
|
+
* family-local state machines. See skills/docs/conductor-routing-contract.md.
|
|
12
4
|
*
|
|
13
5
|
* Contract guarantees:
|
|
14
|
-
* - One deterministic routing outcome per normalized input set
|
|
6
|
+
* - One deterministic routing outcome per normalized input set.
|
|
15
7
|
* - Ambiguous, conflicting, or insufficient inputs return `needs_reconcile`
|
|
16
|
-
* rather than a guessed handoff
|
|
17
|
-
* - The evaluator is purely functional; no I/O or side effects
|
|
18
|
-
* -
|
|
8
|
+
* rather than a guessed handoff.
|
|
9
|
+
* - The evaluator is purely functional; no I/O or side effects.
|
|
10
|
+
* - evaluateConductorRouting is the single routing authority.
|
|
19
11
|
*
|
|
20
|
-
* Integration boundary
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
24
|
-
* pre-computed outer-loop action as an input
|
|
25
|
-
* -
|
|
26
|
-
* - Ownership/idempotency rules remain in conductor-ownership.mjs
|
|
27
|
-
*
|
|
12
|
+
* Integration boundary:
|
|
13
|
+
* - Starts after active-run identity and ownership are already resolved; it
|
|
14
|
+
* consumes already-detected family-local lifecycle states as inputs.
|
|
15
|
+
* - Derives the routing outcome directly from states; it does not take a
|
|
16
|
+
* pre-computed outer-loop action as an input.
|
|
17
|
+
* - Emits routing decisions and handoff envelopes; it does not perform handoff.
|
|
18
|
+
* - Ownership/idempotency rules remain in conductor-ownership.mjs; family-local
|
|
19
|
+
* state machine semantics remain in copilot-loop-state.mjs etc.
|
|
28
20
|
*/
|
|
29
21
|
|
|
30
22
|
// ---------------------------------------------------------------------------
|
|
@@ -580,7 +572,7 @@ function routeFromStates({
|
|
|
580
572
|
* @param {{ repo: string, pr: number }} input.target
|
|
581
573
|
* Explicit target identity (already resolved by the caller).
|
|
582
574
|
* @param {string} [input.ownershipState]
|
|
583
|
-
* Settled ownership/idempotency classification from conductor-ownership
|
|
575
|
+
* Settled ownership/idempotency classification from conductor-ownership.
|
|
584
576
|
* "live_owner" → stay_with_current_live_owner (no new handoff this cycle).
|
|
585
577
|
* "duplicate_local_owners" → needs_reconcile.
|
|
586
578
|
* Other values or omission → routing continues from states.
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deterministic state machine for the async Copilot review/fix loop.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - normalizeSnapshot: validate and canonicalize a raw loop-state snapshot
|
|
8
|
-
* - interpretLoopState: map a snapshot to one current state + allowed transitions + next action
|
|
9
|
-
*
|
|
10
|
-
* The state machine owns workflow control.
|
|
11
|
-
* Agent judgment (accept/defer a comment, confirm a fix, decide on another Copilot pass)
|
|
12
|
-
* becomes an explicit bounded input (agentFixStatus) rather than hidden orchestration behavior.
|
|
4
|
+
* The state machine owns workflow control. Agent judgment (accept/defer a
|
|
5
|
+
* comment, confirm a fix, decide on another Copilot pass) enters as an explicit
|
|
6
|
+
* bounded input (agentFixStatus), never as hidden orchestration behavior.
|
|
13
7
|
*/
|
|
14
8
|
|
|
15
9
|
import { deriveLoopCiStatusFromRollup } from "./copilot-ci-status.mjs";
|
|
@@ -76,9 +70,8 @@ export const DISPOSITION = Object.freeze({
|
|
|
76
70
|
});
|
|
77
71
|
|
|
78
72
|
/**
|
|
79
|
-
* Legal transitions for each state.
|
|
80
|
-
*
|
|
81
|
-
* The agent layer selects among allowed transitions; the state machine enforces the graph.
|
|
73
|
+
* Legal transitions for each state. The agent layer selects among allowed
|
|
74
|
+
* transitions; the state machine enforces the graph.
|
|
82
75
|
*/
|
|
83
76
|
export const TRANSITIONS = Object.freeze({
|
|
84
77
|
[STATE.NO_PR]: [],
|
|
@@ -168,12 +161,9 @@ function isBlockedCiStatus(status) {
|
|
|
168
161
|
|
|
169
162
|
/**
|
|
170
163
|
* Single source of truth for whether the Copilot review round cap has been
|
|
171
|
-
* reached
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* detect-pr-gate-coordination-state) MUST call this function too rather than
|
|
175
|
-
* re-deriving `copilotReviewRoundCount >= maxCopilotRounds` locally, so the
|
|
176
|
-
* two never disagree at the cap boundary.
|
|
164
|
+
* reached. Every caller that needs the "cap reached" boolean MUST call this
|
|
165
|
+
* rather than re-derive `copilotReviewRoundCount >= maxCopilotRounds`, so no two
|
|
166
|
+
* callers disagree at the cap boundary.
|
|
177
167
|
*
|
|
178
168
|
* `copilotReviewRoundCount` counts COMPLETED rounds, so `>=` means every
|
|
179
169
|
* permitted round has already happened. `maxCopilotRounds` of `null`/`0`/
|
|
@@ -211,9 +201,9 @@ export function buildSnapshotFromPrFacts({
|
|
|
211
201
|
const prState = typeof prData?.state === "string" ? prData.state.toUpperCase() : "OPEN";
|
|
212
202
|
const prMerged = prState === "MERGED";
|
|
213
203
|
const prClosed = prState === "CLOSED";
|
|
214
|
-
// Default derivation excludes the loop's own gate-evidence check
|
|
215
|
-
//
|
|
216
|
-
//
|
|
204
|
+
// Default derivation excludes the loop's own gate-evidence check so a caller
|
|
205
|
+
// that never threads an explicit ciStatus (e.g. gate-coordination detection)
|
|
206
|
+
// still never treats it as a blocking CI failure.
|
|
217
207
|
const rollupDerivation = deriveLoopCiStatusFromRollup(prData?.statusCheckRollup);
|
|
218
208
|
|
|
219
209
|
return normalizeSnapshot({
|
|
@@ -245,30 +235,17 @@ function isAutoRerequestEligible(snapshot, state) {
|
|
|
245
235
|
|
|
246
236
|
/**
|
|
247
237
|
* Normalize a raw snapshot object into a validated, canonical snapshot.
|
|
238
|
+
* Unknown or invalid field values are replaced with safe defaults. Throws if
|
|
239
|
+
* `raw` is not a non-null object.
|
|
248
240
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* -
|
|
254
|
-
* -
|
|
255
|
-
* -
|
|
256
|
-
*
|
|
257
|
-
* - prClosed {boolean} — whether the PR has been closed without merge
|
|
258
|
-
* - copilotReviewRequestStatus {"requested"|"already-requested"|"unavailable"|"none"|"failed"}
|
|
259
|
-
* — current known Copilot review-request state, or "none" if unknown
|
|
260
|
-
* - copilotReviewPresent {boolean} — whether at least one Copilot review exists on the PR
|
|
261
|
-
* - copilotReviewOnCurrentHead {boolean} — whether a submitted (non-PENDING) Copilot review
|
|
262
|
-
* exists for the current head commit; this alone does not prove the current-head
|
|
263
|
-
* review-request lifecycle is settled, so callers must still check request-state fields
|
|
264
|
-
* - unresolvedThreadCount {number} — total unresolved review-thread count
|
|
265
|
-
* - actionableThreadCount {number} — unresolved threads with non-bot actionable comments
|
|
266
|
-
* - copilotReviewRoundCount {number} — completed Copilot review rounds observed on the PR
|
|
267
|
-
* - ciStatus {"success"|"failure"|"pending"|"none"|"crediblyGreen"} — current CI check rollup status
|
|
268
|
-
* - lastCopilotRoundMaxSignal {"high"|"mid"|"low"|null} — highest signal level across Copilot-authored threads
|
|
269
|
-
* - agentFixStatus {"applied"|null} — agent-provided input: "applied" when code has been fixed
|
|
270
|
-
* - failureDetails {Array<string>} — names of failing visible check-runs from refreshed head-scoped CI evidence
|
|
271
|
-
* - excludedFailureDetails {Array<string>} — names of failing check-runs filtered out by PR-visibility intersection
|
|
241
|
+
* Non-obvious field semantics:
|
|
242
|
+
* - copilotReviewOnCurrentHead: a submitted (non-PENDING) Copilot review exists
|
|
243
|
+
* for the current head. This alone does NOT prove the current-head
|
|
244
|
+
* review-request lifecycle is settled; callers must still check request-state.
|
|
245
|
+
* - copilotReviewRoundCount: COMPLETED Copilot review rounds observed on the PR.
|
|
246
|
+
* - agentFixStatus: agent-provided input; "applied" when code has been fixed.
|
|
247
|
+
* - failureDetails vs excludedFailureDetails: visible failing check-runs vs
|
|
248
|
+
* check-runs filtered out by the PR-visibility intersection.
|
|
272
249
|
*
|
|
273
250
|
* @param {object} raw - raw snapshot input
|
|
274
251
|
* @returns {object} normalized snapshot
|
|
@@ -315,14 +292,12 @@ export function normalizeSnapshot(raw) {
|
|
|
315
292
|
}
|
|
316
293
|
|
|
317
294
|
/**
|
|
318
|
-
* Return the post-request snapshot that
|
|
319
|
-
* once a Copilot review request
|
|
295
|
+
* Return the post-request snapshot that drives the next wait-cycle
|
|
296
|
+
* interpretation once a Copilot review request is issued or confirmed.
|
|
320
297
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* current-head clean-review convergence is cleared for handoff purposes while
|
|
325
|
-
* preserving whether a submitted Copilot review has ever been observed on the PR.
|
|
298
|
+
* A confirmed active request starts a new wait cycle for the current head, so
|
|
299
|
+
* prior current-head clean-review convergence is cleared while whether a
|
|
300
|
+
* submitted Copilot review was ever observed on the PR is preserved.
|
|
326
301
|
*
|
|
327
302
|
* @param {object} snapshot
|
|
328
303
|
* @param {string} reviewRequestStatus
|
|
@@ -363,7 +338,7 @@ export function applyConfirmedReviewRequest(snapshot, reviewRequestStatus) {
|
|
|
363
338
|
* @param {number} [refinementConfig.lowSignalRoundThreshold]
|
|
364
339
|
* @param {number} [refinementConfig.lowSignalMaxComments]
|
|
365
340
|
* @param {number} [refinementConfig.maxCopilotRounds]
|
|
366
|
-
* @param {boolean} [refinementConfig.preApprovalRequireCi] -
|
|
341
|
+
* @param {boolean} [refinementConfig.preApprovalRequireCi] - default true. When false,
|
|
367
342
|
* the pre-approval CI precondition is opted out, so a non-draft PR with a pending/none/failure
|
|
368
343
|
* CI verdict is not routed to waiting_for_ci / blocked_needs_user_decision (it is past the draft gate).
|
|
369
344
|
* @returns {{
|
|
@@ -378,13 +353,11 @@ export function applyConfirmedReviewRequest(snapshot, reviewRequestStatus) {
|
|
|
378
353
|
export function interpretLoopState(snapshot, refinementConfig) {
|
|
379
354
|
const s = normalizeSnapshot(snapshot);
|
|
380
355
|
|
|
381
|
-
// Pre-approval CI opt-out
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
// gate-coordination guards (which already honor this flag) are ever reached.
|
|
387
|
-
// Default true preserves current behavior for every caller that does not thread it.
|
|
356
|
+
// Pre-approval CI opt-out: when `gates.preApproval.requireCi` is false, the CI
|
|
357
|
+
// verdict must not gate progression at the pre-approval boundary. A non-draft
|
|
358
|
+
// PR is past the draft gate, so treat pending/none/failure CI as non-blocking
|
|
359
|
+
// here (a repo with no CI is not routed to WAITING_FOR_CI /
|
|
360
|
+
// BLOCKED_NEEDS_USER_DECISION). Default true preserves prior behavior.
|
|
388
361
|
const preApprovalRequireCi = refinementConfig?.preApprovalRequireCi !== false;
|
|
389
362
|
const ciBlocks = preApprovalRequireCi && isBlockedCiStatus(s.ciStatus);
|
|
390
363
|
const ciWaits = preApprovalRequireCi && isWaitingCiStatus(s.ciStatus);
|
|
@@ -403,35 +376,19 @@ export function interpretLoopState(snapshot, refinementConfig) {
|
|
|
403
376
|
state = STATE.BLOCKED_NEEDS_USER_DECISION;
|
|
404
377
|
}
|
|
405
378
|
|
|
406
|
-
// Round-cap enforcement
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
// in-flight request
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
// in-flight request
|
|
418
|
-
//
|
|
419
|
-
// when threads are clean and CI is green, route to ROUND_CAP_CLEAN_FALLBACK even if
|
|
420
|
-
// copilotReviewRequestStatus is requested/already-requested. Otherwise a lingering
|
|
421
|
-
// assignment would dead-end the loop at WAITING_FOR_COPILOT_REVIEW waiting for a
|
|
422
|
-
// review that can never come (no further round is permitted past the cap). The
|
|
423
|
-
// pre_approval_gate (current-head clean evidence, enforced elsewhere) reviews any
|
|
424
|
-
// post-cap head change, so this proceeds without skipping review of new code.
|
|
425
|
-
//
|
|
426
|
-
// An in-flight request only still blocks the cap block when the PR is NOT clean
|
|
427
|
-
// (unresolved threads or non-green CI) — that legitimately stays in the fix/wait
|
|
428
|
-
// routing below rather than terminating as a clean fallback.
|
|
429
|
-
//
|
|
430
|
-
// Head-advanced handling: even when the head has advanced past the last submitted
|
|
431
|
-
// Copilot review with clean threads and green CI, re-requesting another Copilot pass
|
|
432
|
-
// is forbidden at the cap, so this routes to ROUND_CAP_CLEAN_FALLBACK (not
|
|
433
|
-
// READY_TO_REREQUEST_REVIEW, which would trigger an illegal auto re-request). The
|
|
434
|
-
// pre_approval_gate handles the current head.
|
|
379
|
+
// Round-cap enforcement, gated before the fix/reply-resolve routing below.
|
|
380
|
+
// copilotReviewRoundCount counts COMPLETED rounds, so at `>= maxRounds` every
|
|
381
|
+
// permitted round is done and any lingering in-flight request is for a
|
|
382
|
+
// forbidden over-cap round. Precedence at the cap:
|
|
383
|
+
// - clean PR (clean threads + green CI): ROUND_CAP_CLEAN_FALLBACK, even with
|
|
384
|
+
// a lingering in-flight request or an advanced head — no further round is
|
|
385
|
+
// permitted, so never re-open for re-request or wait on Copilot. Re-opening
|
|
386
|
+
// would dead-end at WAITING_FOR_COPILOT_REVIEW on a review that can never
|
|
387
|
+
// come. The pre_approval_gate (enforced elsewhere) reviews post-cap head
|
|
388
|
+
// changes, so this skips no review of new code.
|
|
389
|
+
// - not clean, no in-flight request: hard stop at ROUND_CAP_REACHED.
|
|
390
|
+
// - not clean WITH an in-flight request: fall through to the normal
|
|
391
|
+
// fix/reply-resolve/wait routing below (no forced clean fallback).
|
|
435
392
|
const maxRounds = refinementConfig?.maxCopilotRounds;
|
|
436
393
|
const reviewInFlight = s.copilotReviewRequestStatus === "requested"
|
|
437
394
|
|| s.copilotReviewRequestStatus === "already-requested";
|
|
@@ -442,16 +399,11 @@ export function interpretLoopState(snapshot, refinementConfig) {
|
|
|
442
399
|
const ciClean = s.ciStatus === "success" || s.ciStatus === "crediblyGreen" || !preApprovalRequireCi;
|
|
443
400
|
const cleanThreads = s.unresolvedThreadCount === 0;
|
|
444
401
|
if (cleanThreads && ciClean) {
|
|
445
|
-
// Clean PR at the cap: proceed to the pre_approval_gate fallback regardless of a
|
|
446
|
-
// lingering Copilot reviewer assignment or an advanced head — no further Copilot
|
|
447
|
-
// round is permitted, so never re-open for re-request or wait on Copilot here.
|
|
448
402
|
state = STATE.ROUND_CAP_CLEAN_FALLBACK;
|
|
449
403
|
} else if (!reviewInFlight) {
|
|
450
|
-
// Not clean and no in-flight request: hard stop at the cap.
|
|
451
404
|
state = STATE.ROUND_CAP_REACHED;
|
|
452
405
|
}
|
|
453
|
-
// Not clean WITH an in-flight request: leave state undecided
|
|
454
|
-
// fix/reply-resolve/wait routing below handles it (do not force a clean fallback).
|
|
406
|
+
// Not clean WITH an in-flight request: leave state undecided for the routing below.
|
|
455
407
|
}
|
|
456
408
|
|
|
457
409
|
if (state === undefined) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (e.g. a doc-only follow-up commit cannot change what a code-correctness angle
|
|
9
9
|
* would find). Carry-forward lets the gate reuse the prior verdict for such
|
|
10
10
|
* angles — but ONLY when it is provably safe. This holds for a findings-present
|
|
11
|
-
* prior verdict too
|
|
11
|
+
* prior verdict too: a fixer push that never touches an angle's
|
|
12
12
|
* surface must not force that angle's OPEN findings to be re-litigated from
|
|
13
13
|
* scratch — the caller carries the prior findings forward unchanged, still
|
|
14
14
|
* open, still blocking. Carry-forward NEVER converts a finding into an
|
|
@@ -65,7 +65,7 @@ import { ALWAYS_INCLUDE, CATEGORY_ANGLE_MAP } from "../analysis/change-classifie
|
|
|
65
65
|
* @type {Record<string, string[]>}
|
|
66
66
|
*/
|
|
67
67
|
const KIND_TO_CATEGORIES = {
|
|
68
|
-
//
|
|
68
|
+
// A docs file is PROSE_PRESENT when it lands on the prose surface, so
|
|
69
69
|
// deslop's carry-forward surface is the `docs` kind (a non-docs delta never
|
|
70
70
|
// re-runs a clean deslop verdict).
|
|
71
71
|
docs: ["DOCS_ONLY", "PROSE_PRESENT"],
|