@renzrollon/interlock 1.5.0 → 1.6.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.
Files changed (43) hide show
  1. package/.claude-plugin/plugin.json +12 -1
  2. package/CHANGELOG.md +104 -0
  3. package/README.md +7 -8
  4. package/bin/interlock +1655 -29
  5. package/hooks/mod.mjs +21 -20
  6. package/hooks/notify-option.mjs +69 -0
  7. package/hooks/preflight.mjs +6 -42
  8. package/lib/conformance.mjs +75 -0
  9. package/lib/coverage.mjs +387 -0
  10. package/lib/doctor.mjs +17 -13
  11. package/lib/drift.mjs +36 -12
  12. package/lib/findings.mjs +40 -3
  13. package/lib/graph/extract/openspec.mjs +6 -15
  14. package/lib/limits.mjs +78 -1
  15. package/lib/merge-confidence.mjs +1302 -0
  16. package/lib/merge-evidence.mjs +527 -0
  17. package/lib/notify.mjs +10 -20
  18. package/lib/outcomes.mjs +26 -5
  19. package/lib/pipeline.mjs +380 -0
  20. package/lib/prompts/dimensions.mjs +24 -2
  21. package/lib/prompts/domain-context.mjs +271 -0
  22. package/lib/prompts/handoff.mjs +32 -1
  23. package/lib/prompts/index.mjs +57 -0
  24. package/lib/prompts/remediate.mjs +29 -5
  25. package/lib/prompts/review.mjs +21 -7
  26. package/lib/prompts/schemas.mjs +2 -0
  27. package/lib/ready.mjs +13 -1
  28. package/lib/receipt.mjs +72 -2
  29. package/lib/review-core.mjs +224 -21
  30. package/lib/risk.mjs +8 -0
  31. package/lib/run-log.mjs +44 -0
  32. package/lib/run.mjs +859 -27
  33. package/lib/spec-audit.mjs +232 -0
  34. package/lib/spec-coverage.mjs +105 -0
  35. package/lib/spec-text.mjs +257 -0
  36. package/lib/verify.mjs +17 -4
  37. package/package.json +1 -1
  38. package/shared/SESSION-SHAPES.md +1 -1
  39. package/shared/TEST-PROFILE.md +39 -2
  40. package/skills/mr/SKILL.md +161 -11
  41. package/skills/review-code/REVIEW.template.md +24 -6
  42. package/skills/review-code/SKILL.md +16 -6
  43. package/skills/review-code/dimensions/domain.md +21 -0
package/lib/limits.mjs CHANGED
@@ -141,6 +141,18 @@ export const LIMITS = {
141
141
  */
142
142
  maxReviewPolicyBytes: 65_536,
143
143
 
144
+ /**
145
+ * Character budget for the living-requirement text one review prompt carries
146
+ * in its `domain` dimension. Whole requirement blocks are added in capability
147
+ * then spec order until the next would exceed this; every block left out is
148
+ * counted in the text and named in the result, never cut mid-requirement. It
149
+ * bounds a prompt, not a merge decision, so it lives here and not in
150
+ * `MERGE_BANDS`. Read by `assembleDomainContext` in
151
+ * `lib/prompts/domain-context.mjs`.
152
+ * See judge-against-base-and-living-specs design.md D4.
153
+ */
154
+ maxDomainSpecChars: 12_000,
155
+
144
156
  /**
145
157
  * Milliseconds a close's push to the ntfy relay may take before it is
146
158
  * abandoned as a failed push. A hanging network call must not stall the
@@ -512,6 +524,54 @@ export const SOLO = {
512
524
  maxTasks: 20
513
525
  }
514
526
 
527
+ /**
528
+ * The thresholds the merge verdict applies (`interlock merge-confidence`,
529
+ * `lib/merge-confidence.mjs`), published here so no skill, prompt or document
530
+ * restates one as the rule — the reason this module exists.
531
+ *
532
+ * A separate export from LIMITS, like LANE_CAPS and EVAL_CAPS, for two reasons.
533
+ * `ceiling` is a table, not a positive-integer loop count, and the LIMITS
534
+ * invariant holds every entry to that shape. And these are not loop caps: the
535
+ * verdict is the worst band over every signal, and these numbers only decide
536
+ * which band one signal reads, or where inside its band's range a score lands.
537
+ *
538
+ * `ceiling` is the score each band may reach; a band's floor is one above the
539
+ * next-worse band's ceiling, and red's floor is 0. `unobserved` has no row: it
540
+ * takes amber's range, as it takes amber's rank for the verdict, so a fourth
541
+ * number would be one nobody reads. The domain-spec text cap the epic lists is
542
+ * deliberately absent: its reader lands with the `domain` review dimension, and a
543
+ * printed cap with no reader fails the cap-authority sweep.
544
+ *
545
+ * Read by `lib/merge-confidence.mjs`, which cites each key by name in the reason
546
+ * it applies it in; printed by `interlock limits` and emitted as `mergeBands` by
547
+ * `--json`. The cap-authority sweep covers the group, `ceiling` counting as one
548
+ * cap on the terms `LANE_CAPS.byTier` does.
549
+ */
550
+ export const MERGE_BANDS = Object.freeze({
551
+ ceiling: Object.freeze({ green: 100, amber: 79, red: 39 }),
552
+
553
+ /** Surviving review warnings at or under this read green; one more is amber. */
554
+ maxWarningsForMerge: 2,
555
+
556
+ /** Skeptic verdicts a surviving blocker or warning needs before it counts as adjudicated. */
557
+ minVerdictsPerFinding: 2,
558
+
559
+ /** Points the project coverage total may fall below the base total and still read green. */
560
+ maxCoverageDeclinePts: 1,
561
+
562
+ /** Changed-line coverage, in percent, a change with no baseline must reach to read green. */
563
+ patchCoverageFloorPct: 80,
564
+
565
+ /** Changed lines below which coverage is not judged at all; a change of exactly this many is. */
566
+ minChangedLinesForCoverage: 20,
567
+
568
+ /** Changed lines, added plus deleted, above which the diff shape is amber. */
569
+ maxChangedLinesForMerge: 400,
570
+
571
+ /** Changed files above which the diff shape is amber. */
572
+ maxChangedFilesForMerge: 20
573
+ })
574
+
515
575
  /**
516
576
  * Caps for the corpus reader (`interlock report`).
517
577
  *
@@ -573,6 +633,7 @@ export function formatLimits({ runtimeObserved } = {}) {
573
633
  ['verify preview budget (chars)', LIMITS.verifyPreviewChars],
574
634
  ['wave handoff budget (chars, per task)', LIMITS.maxHandoffChars],
575
635
  ['review policy scan cap (bytes)', LIMITS.maxReviewPolicyBytes],
636
+ ['domain spec text budget (chars)', LIMITS.maxDomainSpecChars],
576
637
  ['push timeout (ms)', LIMITS.notifyTimeoutMs],
577
638
  ['resume card list rows (per list)', LIMITS.resumeCardListRows],
578
639
  ['launch ledger max age (ms)', LIMITS.launchLedgerMaxAgeMs],
@@ -623,6 +684,20 @@ export function formatLimits({ runtimeObserved } = {}) {
623
684
  .map(tier => [`cache write multiplier: ${tier} (x input)`, MODEL_PRICES.cacheMultipliers.write[tier]])
624
685
  ]
625
686
  const reportRows = [['report trajectory scan cap', REPORT_CAPS.maxRunsScanned]]
687
+ // One row per ceiling, then one per threshold. `unobserved` prints beside
688
+ // amber because it takes amber's range; it has no number of its own.
689
+ const mergeRows = [
690
+ ['merge band ceiling: green', MERGE_BANDS.ceiling.green],
691
+ ['merge band ceiling: amber (also unobserved)', MERGE_BANDS.ceiling.amber],
692
+ ['merge band ceiling: red', MERGE_BANDS.ceiling.red],
693
+ ['merge: max surviving warnings', MERGE_BANDS.maxWarningsForMerge],
694
+ ['merge: min skeptic verdicts per finding', MERGE_BANDS.minVerdictsPerFinding],
695
+ ['merge: max coverage decline (points)', MERGE_BANDS.maxCoverageDeclinePts],
696
+ ['merge: patch coverage floor (%, no baseline)', MERGE_BANDS.patchCoverageFloorPct],
697
+ ['merge: min changed lines to judge coverage', MERGE_BANDS.minChangedLinesForCoverage],
698
+ ['merge: max changed lines', MERGE_BANDS.maxChangedLinesForMerge],
699
+ ['merge: max changed files', MERGE_BANDS.maxChangedFilesForMerge]
700
+ ]
626
701
  // One row per tier, rather than a rendered object: an operator reading this
627
702
  // wants to know what bounds the lane in front of them, and a tier is how they
628
703
  // find it. The cohesion ceiling and the solo envelope print beside them because
@@ -650,7 +725,7 @@ export function formatLimits({ runtimeObserved } = {}) {
650
725
  ['effort: review skeptic step', EFFORT.skeptic]
651
726
  ]
652
727
  const width = Math.max(
653
- ...[...rows, ...laneRows, ...evalRows, ...priceRows, ...reportRows, ...effortRows].map(
728
+ ...[...rows, ...laneRows, ...mergeRows, ...evalRows, ...priceRows, ...reportRows, ...effortRows].map(
654
729
  ([label]) => label.length
655
730
  )
656
731
  )
@@ -660,6 +735,8 @@ export function formatLimits({ runtimeObserved } = {}) {
660
735
  '\n\n' +
661
736
  laneRows.map(fmt).join('\n') +
662
737
  '\n\n' +
738
+ mergeRows.map(fmt).join('\n') +
739
+ '\n\n' +
663
740
  evalRows.map(fmt).join('\n') +
664
741
  '\n\n' +
665
742
  priceRows.map(fmt).join('\n') +