@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.
@@ -1,30 +1,22 @@
1
1
  /**
2
- * Conductor routing contract: deterministic routing and handoff decisions
3
- * above family-local state machines.
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
- * - Callers use evaluateConductorRouting as the single routing authority
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 (see skills/docs/conductor-routing-contract.md):
21
- * - This module starts after active-run identity and ownership are already resolved
22
- * - It consumes already-detected family-local lifecycle states as inputs
23
- * - It derives the routing outcome directly from states; it does not take a
24
- * pre-computed outer-loop action as an input
25
- * - It emits routing decisions and handoff envelopes; it does not perform handoff
26
- * - Ownership/idempotency rules remain in conductor-ownership.mjs (#32)
27
- * - Family-local state machine semantics remain in copilot-loop-state.mjs etc. (#26)
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 (#32).
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
- * This module provides:
5
- * - STATE: stable state name constants
6
- * - TRANSITIONS: legal next-state graph for each state
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
- * Each entry lists the states that are reachable from the given state.
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 (issue #1126). `copilot-pr-handoff.mjs` enforces the cap by calling
172
- * `interpretLoopState`, which uses this predicate internally; every other
173
- * caller that needs the same "cap reached" boolean (gate coordination,
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 (#1358) so a
215
- // caller that never threads an explicit ciStatus (e.g. gate-coordination
216
- // detection) still never treats it as a blocking CI failure.
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
- * Unknown or invalid field values are replaced with safe defaults.
250
- * Throws if `raw` is not a non-null object.
251
- *
252
- * Snapshot schema:
253
- * - prExists {boolean} whether a PR was found
254
- * - prNumber {number|null} PR number if prExists, otherwise null
255
- * - prDraft {boolean} whether the PR is in draft state
256
- * - prMerged {boolean} whether the PR has been merged
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 should drive the next wait-cycle interpretation
319
- * once a Copilot review request has been explicitly issued or confirmed.
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
- * This keeps the handoff helper on the same shared state-machine contract instead of
322
- * emitting a watch action that contradicts a same-head clean-convergence interpretation.
323
- * A confirmed request starts a new wait cycle for the current head, so prior
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] - #1337: default true. When false,
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 (#1337): when `gates.preApproval.requireCi` is false,
382
- // the CI verdict must not gate progression at the pre-approval boundary. A
383
- // non-draft PR is past the draft gate, so this is the applicable knob — treat
384
- // pending/none/failure CI as non-blocking here so a repo with no CI is not
385
- // routed to WAITING_FOR_CI / BLOCKED_NEEDS_USER_DECISION before the downstream
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: when maxCopilotRounds is configured and the review-round
407
- // count has been exhausted, stop re-requests before entering fix/reply-resolve routing.
408
- // Gating here (before unresolved-thread checks) lets a CLEAN PR at the cap terminate as
409
- // ROUND_CAP_CLEAN_FALLBACK ahead of the normal fix/wait routing. It does NOT blanket-
410
- // override that routing: a NOT-clean PR (unresolved threads or non-green CI) with an
411
- // in-flight request deliberately falls through to the normal fix/wait routing below
412
- // (see the `!reviewInFlight` branch), and only a not-clean PR with no in-flight request
413
- // hard-stops at ROUND_CAP_REACHED.
414
- //
415
- // Precedence at the cap: copilotReviewRoundCount counts COMPLETED rounds, so at
416
- // `>= maxRounds` every permitted Copilot round is already done and any lingering
417
- // in-flight request (requested/already-requested) is for a forbidden over-cap round.
418
- // A stale Copilot reviewer assignment must therefore NOT block the clean fallback:
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 so the normal
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 (issue #2017): a fixer push that never touches an angle's
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
- // #1442: a docs file is PROSE_PRESENT when it lands on the prose surface, so
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"],