@codyswann/lisa 4.54.5 → 4.54.7

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 (114) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +8 -0
  3. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  4. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +13 -11
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/package.json +4 -4
  9. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +19 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-github-verify/SKILL.md +17 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +35 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-jira-verify/SKILL.md +17 -0
  16. package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +35 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +19 -0
  18. package/plugins/lisa/.codex-plugin/skills/lisa-linear-verify/SKILL.md +17 -0
  19. package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +35 -0
  20. package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +19 -0
  21. package/plugins/lisa/skills/lisa-github-verify/SKILL.md +17 -0
  22. package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +35 -0
  23. package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
  24. package/plugins/lisa/skills/lisa-jira-verify/SKILL.md +17 -0
  25. package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +35 -0
  26. package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +19 -0
  27. package/plugins/lisa/skills/lisa-linear-verify/SKILL.md +17 -0
  28. package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +35 -0
  29. package/plugins/lisa-agy/plugin.json +1 -1
  30. package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +19 -0
  31. package/plugins/lisa-agy/skills/lisa-github-verify/SKILL.md +17 -0
  32. package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +35 -0
  33. package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
  34. package/plugins/lisa-agy/skills/lisa-jira-verify/SKILL.md +17 -0
  35. package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +35 -0
  36. package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +19 -0
  37. package/plugins/lisa-agy/skills/lisa-linear-verify/SKILL.md +17 -0
  38. package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +35 -0
  39. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  42. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-copilot/skills/lisa-github-validate-issue/SKILL.md +19 -0
  46. package/plugins/lisa-copilot/skills/lisa-github-verify/SKILL.md +17 -0
  47. package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +35 -0
  48. package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
  49. package/plugins/lisa-copilot/skills/lisa-jira-verify/SKILL.md +17 -0
  50. package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +35 -0
  51. package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +19 -0
  52. package/plugins/lisa-copilot/skills/lisa-linear-verify/SKILL.md +17 -0
  53. package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +35 -0
  54. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +19 -0
  56. package/plugins/lisa-cursor/skills/lisa-github-verify/SKILL.md +17 -0
  57. package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +35 -0
  58. package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
  59. package/plugins/lisa-cursor/skills/lisa-jira-verify/SKILL.md +17 -0
  60. package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +35 -0
  61. package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +19 -0
  62. package/plugins/lisa-cursor/skills/lisa-linear-verify/SKILL.md +17 -0
  63. package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +35 -0
  64. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-expo-agy/plugin.json +1 -1
  67. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  72. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  76. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  77. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  81. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  82. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  86. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  87. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  91. package/plugins/lisa-rails-agy/plugin.json +1 -1
  92. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  96. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  97. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  101. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  102. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  104. package/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +19 -0
  105. package/plugins/src/base/skills/lisa-github-verify/SKILL.md +17 -0
  106. package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +35 -0
  107. package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +19 -0
  108. package/plugins/src/base/skills/lisa-jira-verify/SKILL.md +17 -0
  109. package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +35 -0
  110. package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +19 -0
  111. package/plugins/src/base/skills/lisa-linear-verify/SKILL.md +17 -0
  112. package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +35 -0
  113. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +326 -16
  114. package/typescript/create-only/.github/workflows/review-evidence.yml +29 -0
@@ -1632,6 +1632,16 @@ export const REVIEW_GATE_STATES = Object.freeze({
1632
1632
  * | `unrecognised` | the reviewer said something new | classify the phrase |
1633
1633
  * | `undetermined` | THE GATE STOPPED WAITING | re-run; read nothing into it |
1634
1634
  *
1635
+ * `waived_with_review` is the eighth, and it is a WAIVER — same severity, same
1636
+ * `neutral`, same never-blocking kind. It exists because the waiver's sentence
1637
+ * was a claim rather than a label: "this pull request is UNREVIEWED". MEASURED
1638
+ * on this repository 2026-09-06 over the last 40 merged pull requests, 34 of
1639
+ * which reported `Review rate limited`: SEVEN of those 34 carried review
1640
+ * activity at the head. The waiver described one waived merge in five wrongly,
1641
+ * and it described it wrongly in the direction that matters — an operator
1642
+ * writing a merge disclosure from that sentence asserts nobody looked while a
1643
+ * finding sits on the pull request.
1644
+ *
1635
1645
  * All of them published one word, so an operator handed `unsatisfied` had to
1636
1646
  * guess between situations whose correct responses have nothing in common. That
1637
1647
  * collapse is why CodySwannGT/lisa#3706, #3716 and #3600 each described a
@@ -1666,6 +1676,7 @@ export const REVIEW_GATE_STATES = Object.freeze({
1666
1676
  export const REVIEW_GATE_CONDITIONS = Object.freeze({
1667
1677
  satisfied: "satisfied",
1668
1678
  waived: "waived",
1679
+ waivedWithReview: "waived_with_review",
1669
1680
  absent: "absent",
1670
1681
  objected: "objected",
1671
1682
  pending: "pending",
@@ -1673,6 +1684,105 @@ export const REVIEW_GATE_CONDITIONS = Object.freeze({
1673
1684
  undetermined: "undetermined",
1674
1685
  });
1675
1686
 
1687
+ /**
1688
+ * What the pull request's OWN review surface says, independent of any check.
1689
+ *
1690
+ * ## Why a second input exists at all (#3706)
1691
+ *
1692
+ * The gate classified an evidence-bearing check from its `state` and its
1693
+ * `description`, and **the description is not a function of whether a review
1694
+ * happened.** Measured on CodySwannGT/lisa#3762: `success` with the
1695
+ * description `Review rate limited`, and the vendor HAD reviewed, posting
1696
+ * `CHANGES_REQUESTED` with a comment that found a real defect. The identical
1697
+ * string appears on pull requests nothing read. One string, two opposite
1698
+ * facts — and the waiver collapsed them PERMISSIVELY, so a pull request a
1699
+ * reviewer objected to published `neutral` and merged.
1700
+ *
1701
+ * No rearrangement of the description vocabulary can fix that, because the
1702
+ * information is not in the description. It is in `reviewDecision` and
1703
+ * `reviewThreads`, which `gh pr checks` cannot see and this gate did not read.
1704
+ *
1705
+ * ## Why `reviewDecision` rather than the reviews' own states
1706
+ *
1707
+ * `reviewDecision` is GitHub's own computation of the latest state per
1708
+ * reviewer, so a DISMISSED objection and a superseded one are already excluded.
1709
+ * Recomputing it here from `reviews[].state` would resurrect a stale
1710
+ * `CHANGES_REQUESTED` and block work that was already addressed — the false-red
1711
+ * direction tracked on #3720, which this must not manufacture while closing the
1712
+ * false-green one.
1713
+ *
1714
+ * ## COULD-NOT-ASK IS A THIRD STATE, NOT AN ABSENCE VALUE
1715
+ *
1716
+ * "No review activity" is what GRANTS the waiver, so a failed read resolving to
1717
+ * it would waive on evidence nobody obtained — this file's own thesis, one
1718
+ * square over, and the shape `scripts/check-probe-absence-direction.mjs`
1719
+ * refuses outright (#3848): a probe must not hand a failure back as an answer.
1720
+ * The first draft here returned `undefined` on a failed read, which the caller
1721
+ * did distinguish — and which is still the same token a caller elsewhere reads
1722
+ * as a legitimate negative. So the third state is carried in the value:
1723
+ * `read: false` is a fact about the READ, and `present` / `objected` are facts
1724
+ * about the pull request that only mean anything when `read` is true.
1725
+ *
1726
+ * @param {unknown} payload - `pullRequest` as the GraphQL read returns it
1727
+ * @returns {{read: boolean, present: boolean, objected: boolean}} What the review surface showed, and whether it was read at all
1728
+ */
1729
+ export function reviewActivityFrom(payload) {
1730
+ if (typeof payload !== "object" || payload === null) {
1731
+ return UNREAD_REVIEW_ACTIVITY;
1732
+ }
1733
+ const reviews = /** @type {{totalCount?: unknown}} */ (
1734
+ /** @type {Record<string, unknown>} */ (payload).reviews
1735
+ );
1736
+ const threads = /** @type {{totalCount?: unknown, nodes?: unknown}} */ (
1737
+ /** @type {Record<string, unknown>} */ (payload).reviewThreads
1738
+ );
1739
+ if (typeof reviews?.totalCount !== "number") return UNREAD_REVIEW_ACTIVITY;
1740
+ if (typeof threads?.totalCount !== "number") return UNREAD_REVIEW_ACTIVITY;
1741
+ const nodes = Array.isArray(threads.nodes) ? threads.nodes : [];
1742
+ const unresolved = nodes.some(node => node?.isResolved === false);
1743
+ const decision = String(
1744
+ /** @type {Record<string, unknown>} */ (payload).reviewDecision ?? ""
1745
+ ).toUpperCase();
1746
+ const objected = decision === "CHANGES_REQUESTED" || unresolved;
1747
+ // The query asks for the first 50 threads, and the two verdicts do not need
1748
+ // the same evidence.
1749
+ //
1750
+ // "Somebody objected" is sound from a partial page: a thread we did not read
1751
+ // cannot un-object the one we did. "Nobody objected" is not — it is a claim
1752
+ // about every thread, and a page of 50 resolved ones says nothing about the
1753
+ // fifty-first. A pull request with more than 50 threads whose only unresolved
1754
+ // one sorts late would otherwise report `objected: false`, and a neutral
1755
+ // waiver would be allowed to cover a live objection.
1756
+ //
1757
+ // `totalCount` is already in the response, so the shortfall is detectable
1758
+ // without a second request. Reporting it as UNREAD is the honest answer and
1759
+ // the one this module is built around: a failed read is NOT a negative, and
1760
+ // the cost lands on the waiver's corroboration rather than on the merge.
1761
+ if (!objected && nodes.length < threads.totalCount) {
1762
+ return UNREAD_REVIEW_ACTIVITY;
1763
+ }
1764
+ return {
1765
+ read: true,
1766
+ present: reviews.totalCount > 0 || threads.totalCount > 0,
1767
+ objected,
1768
+ };
1769
+ }
1770
+
1771
+ /**
1772
+ * The review surface as it looks when nobody managed to read it.
1773
+ *
1774
+ * A named value rather than `undefined` on purpose: `undefined` is what a
1775
+ * caller elsewhere in this file reads as a legitimate negative, and the whole
1776
+ * point is that a failed read is NOT a negative. `present` and `objected` are
1777
+ * `false` here only because the shape requires values; `read: false` is the
1778
+ * field every consumer must branch on first.
1779
+ */
1780
+ export const UNREAD_REVIEW_ACTIVITY = Object.freeze({
1781
+ read: false,
1782
+ present: false,
1783
+ objected: false,
1784
+ });
1785
+
1676
1786
  /**
1677
1787
  * Normalises a description for whole-string comparison.
1678
1788
  *
@@ -1719,7 +1829,13 @@ function normalizeDescription(description) {
1719
1829
  * long" are different things to go and look at; what changes is that neither is
1720
1830
  * asserted as a fact about the reviewer when the gate simply stopped waiting.
1721
1831
  *
1722
- * @param {{present: boolean, state?: string, description?: string, waitExpired?: boolean}} reading - One check, and whether the settle wait expired
1832
+ * A WAIVER IS NOW CONDITIONAL ON THE ABSENCE OF REVIEW (#3706). `reading.
1833
+ * reviewActivity` is what the pull request's own review surface showed, and it
1834
+ * only ever reaches the waiver branch — the satisfy, absent, pending and
1835
+ * unrecognised paths are untouched by it, deliberately, because none of them
1836
+ * grants permission to merge on a claim about who reviewed.
1837
+ *
1838
+ * @param {{present: boolean, state?: string, description?: string, waitExpired?: boolean, reviewActivity?: {read: boolean, present: boolean, objected: boolean}}} reading - One check, whether the settle wait expired, and what the pull request's own review surface showed
1723
1839
  * @param {{waive?: readonly string[], satisfy?: readonly string[]}} [vocabulary] - Per-check extensions
1724
1840
  * @returns {{state: string, condition: string, why: string}} Severity, the condition observed, and a one-line reason
1725
1841
  */
@@ -1734,7 +1850,12 @@ export function reviewGateState(reading, vocabulary = {}) {
1734
1850
  if (state !== "SUCCESS") {
1735
1851
  return pendingReviewGateState(state, reading.waitExpired);
1736
1852
  }
1737
- return successfulReviewGateState(text, reading.description, vocabulary);
1853
+ return successfulReviewGateState(
1854
+ text,
1855
+ reading.description,
1856
+ vocabulary,
1857
+ reading.reviewActivity
1858
+ );
1738
1859
  }
1739
1860
 
1740
1861
  /**
@@ -1803,15 +1924,70 @@ function pendingReviewGateState(state, waitExpired) {
1803
1924
  };
1804
1925
  }
1805
1926
 
1927
+ /**
1928
+ * Classifies a check whose description matched an entitlement waiver.
1929
+ *
1930
+ * ## The waiver keeps its severity and loses its false claim (#3706)
1931
+ *
1932
+ * The owner's ruling on #3221, restated on #3706, is untouched: a pull request
1933
+ * author cannot fix a vendor entitlement, so a throttled reviewer must not
1934
+ * redden the author's pull request. That ruling is about the pull request
1935
+ * NOBODY REVIEWED, which is the only pull request it was ever an argument
1936
+ * about — and the third branch below is where it lives, unchanged.
1937
+ *
1938
+ * What the ruling never covered is a pull request where a review DID happen and
1939
+ * OBJECTED. Measured on #3762: `success`, description `Review rate limited`,
1940
+ * and a `CHANGES_REQUESTED` finding a real defect. The waiver covered it
1941
+ * anyway, because the gate read only the string — so the one condition the file
1942
+ * calls "the case this gate exists to let through to a human" was the condition
1943
+ * it merged. That is the first branch, and it is the fix.
1944
+ *
1945
+ * The second branch is the honesty half. Review activity WITHOUT an objection
1946
+ * still waives — the entitlement argument still applies and the severity does
1947
+ * not move — but the sentence stops asserting "this pull request is
1948
+ * UNREVIEWED", which was measured false on 7 of 34 waived merges here.
1949
+ *
1950
+ * @param {string|undefined} description - Original description
1951
+ * @param {{read: boolean, present: boolean, objected: boolean}|undefined} activity - What the pull request's own review surface showed
1952
+ * @returns {{state: string, condition: string, why: string}} Review gate verdict
1953
+ */
1954
+ function waivedReviewGateState(description, activity) {
1955
+ const said = `reported ${JSON.stringify(description ?? "")}`;
1956
+ const read = activity !== undefined && activity.read !== false;
1957
+ if (read && activity.objected === true) {
1958
+ return {
1959
+ state: REVIEW_GATE_STATES.unsatisfied,
1960
+ condition: REVIEW_GATE_CONDITIONS.objected,
1961
+ why: `${said} — but a review HAPPENED on this pull request and OBJECTED to it: \`reviewDecision\` is CHANGES_REQUESTED, or a review thread is unresolved. NO WAIVER COVERS AN OBJECTION. The description said the reviewer could not review and the review surface says it did; the review surface is the authority, because a vendor's throttle message is not a function of whether anybody read the diff (CodySwannGT/lisa#3762). READ THE OBJECTION.`,
1962
+ };
1963
+ }
1964
+ if (read && activity.present === true) {
1965
+ return {
1966
+ state: REVIEW_GATE_STATES.waived,
1967
+ condition: REVIEW_GATE_CONDITIONS.waivedWithReview,
1968
+ why: `${said} — the check saying, in its own words, that it could not review. WAIVED: merging is a decision taken on that basis. But this pull request DOES carry review activity that raised no objection, so it is not the unreviewed pull request the waiver's rationale is about — do not record it as one.`,
1969
+ };
1970
+ }
1971
+ const unverified = read
1972
+ ? ""
1973
+ : " The pull request's own review surface could not be read on this run, so nothing corroborated the waiver: it is granted on the description alone, which is the input measured to be wrong in both directions.";
1974
+ return {
1975
+ state: REVIEW_GATE_STATES.waived,
1976
+ condition: REVIEW_GATE_CONDITIONS.waived,
1977
+ why: `${said} — the check saying, in its own words, that it could not review. WAIVED, not satisfied: this pull request is UNREVIEWED and merging it is a decision taken on that basis.${unverified} The waiver clears the moment the entitlement behind it is fixed, at which point this gate starts biting with no code change.`,
1978
+ };
1979
+ }
1980
+
1806
1981
  /**
1807
1982
  * Classifies a successful check from its exact description vocabulary.
1808
1983
  *
1809
1984
  * @param {string} text - Normalized description
1810
1985
  * @param {string|undefined} description - Original description
1811
1986
  * @param {{waive?: readonly string[], satisfy?: readonly string[]}} vocabulary - Per-check extensions
1987
+ * @param {{read: boolean, present: boolean, objected: boolean}|undefined} [activity] - What the pull request's own review surface showed
1812
1988
  * @returns {{state: string, condition: string, why: string}} Review gate verdict
1813
1989
  */
1814
- function successfulReviewGateState(text, description, vocabulary) {
1990
+ function successfulReviewGateState(text, description, vocabulary, activity) {
1815
1991
  const satisfies = [...REVIEW_SATISFACTIONS, ...(vocabulary.satisfy ?? [])];
1816
1992
  if (satisfies.some(phrase => text === normalizeDescription(phrase))) {
1817
1993
  return {
@@ -1822,11 +1998,7 @@ function successfulReviewGateState(text, description, vocabulary) {
1822
1998
  }
1823
1999
  const waivers = [...ENTITLEMENT_WAIVERS, ...(vocabulary.waive ?? [])];
1824
2000
  if (waivers.some(phrase => text === normalizeDescription(phrase))) {
1825
- return {
1826
- state: REVIEW_GATE_STATES.waived,
1827
- condition: REVIEW_GATE_CONDITIONS.waived,
1828
- why: `reported ${JSON.stringify(description ?? "")} — the check saying, in its own words, that it could not review. WAIVED, not satisfied: this pull request is UNREVIEWED and merging it is a decision taken on that basis. The waiver clears the moment the entitlement behind it is fixed, at which point this gate starts biting with no code change.`,
1829
- };
2001
+ return waivedReviewGateState(description, activity);
1830
2002
  }
1831
2003
  return {
1832
2004
  state: REVIEW_GATE_STATES.unsatisfied,
@@ -1845,7 +2017,7 @@ function successfulReviewGateState(text, description, vocabulary) {
1845
2017
  *
1846
2018
  * @param {object} declaration - The per-repo declaration
1847
2019
  * @param {ReadonlyArray<{name: string, state: string, description?: string}>} checks - The checks
1848
- * @param {{headSha?: string, waitExpired?: boolean}} [options] - `headSha` is cited in every finding; `waitExpired` says the settle loop hit its deadline
2020
+ * @param {{headSha?: string, waitExpired?: boolean, reviewActivity?: {read: boolean, present: boolean, objected: boolean}}} [options] - `headSha` is cited in every finding; `waitExpired` says the settle loop hit its deadline; `reviewActivity` is what the pull request's own review surface showed, absent or `read: false` when it was never read
1849
2021
  * @returns {{violations: object[], states: Record<string, string>, conditions: Record<string, string>, descriptions: Record<string, string>, checked: number}} Findings, per-check state and condition, the description each verdict was read from, and how many were examined
1850
2022
  */
1851
2023
  export function evaluateReviewGate(declaration, checks, options = {}) {
@@ -1877,6 +2049,7 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
1877
2049
  state: found.state,
1878
2050
  description: found.description,
1879
2051
  waitExpired: options.waitExpired === true,
2052
+ reviewActivity: options.reviewActivity,
1880
2053
  },
1881
2054
  vocabulary
1882
2055
  );
@@ -2122,6 +2295,28 @@ function carriedSuffix(carried) {
2122
2295
  * pull requests this one CARRIES
2123
2296
  * @returns {{verdict: string, conclusion: string, title: string}} What to publish
2124
2297
  */
2298
+ /**
2299
+ * The lead sentence for an unsatisfied verdict, keyed on WHAT was observed.
2300
+ *
2301
+ * Three leads because the three conditions hand an operator three different
2302
+ * facts, and the title is the one surface a merge decision reads. Split out of
2303
+ * {@link reviewGateVerdict} so each lead is one expression rather than a nested
2304
+ * ternary nobody can read.
2305
+ *
2306
+ * @param {{allUndetermined: boolean, allObjected: boolean, unsatisfied: readonly string[], quote: (name: string) => string}} reading - The classification and how to name each check
2307
+ * @returns {string} The lead sentence, without the carried/waive-rate suffix
2308
+ */
2309
+ function unsatisfiedTitle(reading) {
2310
+ const named = reading.unsatisfied.map(reading.quote).join("; ");
2311
+ if (reading.allUndetermined) {
2312
+ return `UNDETERMINED — the gate stopped waiting before review evidence settled, and is NOT reporting that nobody reviewed: ${named}. RE-RUN this job; do not investigate the change on the strength of this`;
2313
+ }
2314
+ if (reading.allObjected) {
2315
+ return `OBJECTED — a review HAPPENED on this pull request and objected to it; this is NOT an unreviewed pull request and no waiver covers it: ${named}. READ THE OBJECTION`;
2316
+ }
2317
+ return `UNREVIEWED — review evidence unsatisfied: ${named}`;
2318
+ }
2319
+
2125
2320
  export function reviewGateVerdict(reading = {}) {
2126
2321
  const states = reading.states ?? {};
2127
2322
  const conditions = reading.conditions ?? {};
@@ -2179,13 +2374,23 @@ export function reviewGateVerdict(reading = {}) {
2179
2374
  const allUndetermined = unsatisfied.every(
2180
2375
  name => conditions[name] === REVIEW_GATE_CONDITIONS.undetermined
2181
2376
  );
2377
+ // AND THE SAME MISTAKE ONE ROW DOWN (#3706). `objected` is the one
2378
+ // unsatisfying condition where a review DID happen, and this title called
2379
+ // it UNREVIEWED — the word an operator quotes into a merge disclosure. It
2380
+ // is the more expensive direction of the two: `undetermined` sends someone
2381
+ // to audit a change nobody had a finding about, while UNREVIEWED-on-an-
2382
+ // objection tells them no finding exists while one sits on the pull
2383
+ // request. Only when EVERY unsatisfied check objected, so a real silence
2384
+ // alongside an objection still leads with UNREVIEWED.
2385
+ const allObjected = unsatisfied.every(
2386
+ name => conditions[name] === REVIEW_GATE_CONDITIONS.objected
2387
+ );
2182
2388
  return {
2183
2389
  verdict: REVIEW_GATE_STATES.unsatisfied,
2184
2390
  conclusion: REVIEW_VERDICT_CONCLUSIONS.unsatisfied,
2185
2391
  title: fitTitle(
2186
- allUndetermined
2187
- ? `UNDETERMINED — the gate stopped waiting before review evidence settled, and is NOT reporting that nobody reviewed: ${unsatisfied.map(quote).join("; ")}. RE-RUN this job; do not investigate the change on the strength of this${suffix}`
2188
- : `UNREVIEWED — review evidence unsatisfied: ${unsatisfied.map(quote).join("; ")}${suffix}`
2392
+ unsatisfiedTitle({ allUndetermined, allObjected, unsatisfied, quote }) +
2393
+ suffix
2189
2394
  ),
2190
2395
  };
2191
2396
  }
@@ -2194,11 +2399,24 @@ export function reviewGateVerdict(reading = {}) {
2194
2399
  name => states[name] === REVIEW_GATE_STATES.waived
2195
2400
  );
2196
2401
  if (waived.length > 0) {
2402
+ // AND THE TITLE IS WHERE THE CLAIM IS READ (#3706). "This pull request is
2403
+ // UNREVIEWED" is the sentence an operator quotes into a merge disclosure,
2404
+ // and it was printed unconditionally — measured false on 7 of the last 34
2405
+ // waived merges here, every one of which carried review activity at the
2406
+ // head. Severity does not move: both leads publish `neutral`, and the
2407
+ // waiver's rationale is untouched. Only the claim changes, and only when
2408
+ // EVERY waived check saw review activity, so one genuinely silent check
2409
+ // alongside a reviewed one still leads with UNREVIEWED.
2410
+ const allWithReview = waived.every(
2411
+ name => conditions[name] === REVIEW_GATE_CONDITIONS.waivedWithReview
2412
+ );
2197
2413
  return {
2198
2414
  verdict: REVIEW_GATE_STATES.waived,
2199
2415
  conclusion: REVIEW_VERDICT_CONCLUSIONS.waived,
2200
2416
  title: fitTitle(
2201
- `WAIVED — this pull request is UNREVIEWED and merging it is a decision taken on that basis: ${waived.map(quote).join("; ")}${suffix}`
2417
+ allWithReview
2418
+ ? `WAIVED — the review check could not review, and merging is a decision taken on that basis; but this pull request DOES carry review activity that raised no objection, so do NOT record it as unreviewed: ${waived.map(quote).join("; ")}${suffix}`
2419
+ : `WAIVED — this pull request is UNREVIEWED and merging it is a decision taken on that basis: ${waived.map(quote).join("; ")}${suffix}`
2202
2420
  ),
2203
2421
  };
2204
2422
  }
@@ -2232,6 +2450,81 @@ export function reviewGateVerdict(reading = {}) {
2232
2450
  };
2233
2451
  }
2234
2452
 
2453
+ /**
2454
+ * The one call that answers "did a review actually happen on this pull request?"
2455
+ *
2456
+ * Everything else this file reads is a CHECK — a conclusion and a description
2457
+ * posted by a vendor about itself. This reads the pull request's own review
2458
+ * surface, which is the only place the answer exists and the one place
2459
+ * `gh pr checks` cannot reach: measured on a pull request in this repository,
2460
+ * `gh pr checks` printed 42 pass / 1 fail / 5 skipping and said nothing about a
2461
+ * blocking `CHANGES_REQUESTED` or an unresolved thread, both of which block a
2462
+ * merge here.
2463
+ *
2464
+ * `first: 50` on the threads because only `isResolved` is read and only whether
2465
+ * ANY is false matters; a pull request with more than fifty threads is already
2466
+ * blocked by the first unresolved one in the page.
2467
+ */
2468
+ const REVIEW_ACTIVITY_QUERY = [
2469
+ "query($owner: String!, $name: String!, $number: Int!) {",
2470
+ " repository(owner: $owner, name: $name) {",
2471
+ " pullRequest(number: $number) {",
2472
+ " reviewDecision",
2473
+ " reviews(first: 1) { totalCount }",
2474
+ " reviewThreads(first: 50) { totalCount nodes { isResolved } }",
2475
+ " }",
2476
+ " }",
2477
+ "}",
2478
+ ].join("\n");
2479
+
2480
+ /**
2481
+ * Reads what the pull request's own review surface shows, or nothing.
2482
+ *
2483
+ * BEST-EFFORT AND NEVER FATAL, in the direction that costs a waiver its
2484
+ * corroboration rather than a pull request its merge: a failed read returns
2485
+ * {@link UNREAD_REVIEW_ACTIVITY}, which {@link waivedReviewGateState} renders
2486
+ * as a waiver that says it was not corroborated. Throwing here would convert a
2487
+ * transient GitHub error into a red review gate on a pull request nobody has
2488
+ * any finding about, on 85% of pull requests here — the "gate that reddens
2489
+ * every pull request and then gets deleted" failure this file names twice.
2490
+ *
2491
+ * probe-direction: fail-closed — a failed read cannot produce
2492
+ * `{read: true, present: false}`, which is the only value that grants an
2493
+ * uncorroborated waiver its silence. It produces `read: false` instead, and the
2494
+ * verdict published then says out loud that nothing corroborated the waiver. No
2495
+ * outcome is quieter for the failure than it would have been for a real read.
2496
+ *
2497
+ * @param {string|number} pr - Pull request number
2498
+ * @param {string} [repo] - `OWNER/NAME`; defaults to the current repository
2499
+ * @returns {{read: boolean, present: boolean, objected: boolean}} What the review surface showed, and whether it was read at all
2500
+ */
2501
+ export function fetchReviewActivity(pr, repo) {
2502
+ const slug = resolveRepoSlug(repo);
2503
+ if (slug === undefined) return UNREAD_REVIEW_ACTIVITY;
2504
+ const [owner, name] = slug.split("/");
2505
+ try {
2506
+ const raw = boundedExecFileSync(
2507
+ "gh",
2508
+ [
2509
+ "api",
2510
+ "graphql",
2511
+ "-f",
2512
+ `query=${REVIEW_ACTIVITY_QUERY}`,
2513
+ "-F",
2514
+ `owner=${owner}`,
2515
+ "-F",
2516
+ `name=${name}`,
2517
+ "-F",
2518
+ `number=${pr}`,
2519
+ ],
2520
+ { encoding: "utf8" }
2521
+ );
2522
+ return reviewActivityFrom(JSON.parse(raw)?.data?.repository?.pullRequest);
2523
+ } catch {
2524
+ return UNREAD_REVIEW_ACTIVITY;
2525
+ }
2526
+ }
2527
+
2235
2528
  /**
2236
2529
  * Counts how a sample of pull requests ended up, worst-wins per pull request.
2237
2530
  *
@@ -3205,7 +3498,7 @@ function readSecondsFlag(argv, name, fallback) {
3205
3498
  *
3206
3499
  * @param {ReadonlyArray<string>} argv - CLI arguments
3207
3500
  * @param {object} declaration - The per-repo declaration
3208
- * @param {{trustRequiredContexts?: boolean, env?: NodeJS.ProcessEnv, fetch?: Function, probeBranch?: Function, now?: Function, sleep?: Function, headSha?: Function}} [options] -
3501
+ * @param {{trustRequiredContexts?: boolean, env?: NodeJS.ProcessEnv, fetch?: Function, probeBranch?: Function, now?: Function, sleep?: Function, headSha?: Function, reviewActivity?: Function}} [options] -
3209
3502
  * Injection seams for the suite
3210
3503
  * @returns {{pr: string|undefined, prSource: string|null, headSha: string|undefined, checked: number, violations: object[], settled: boolean, refusal: {kind: string, reason: string}|null}|undefined} The inspection
3211
3504
  */
@@ -3288,14 +3581,31 @@ export function inspectVacuity(argv, declaration, options = {}) {
3288
3581
  // block. Its findings are what make a waiver visible, and a waiver that is
3289
3582
  // only computed when somebody opted in would be invisible on exactly the
3290
3583
  // repositories that have not opted in yet.
3291
- const gate = evaluateReviewGate(declaration, read.checks, {
3584
+ const gateOptions = {
3292
3585
  headSha: read.headSha,
3293
3586
  // The settle loop exits on one of two things: everything declared reached a
3294
3587
  // terminal read, or the deadline passed. So `settled === false` IS "the
3295
3588
  // wait expired", and it is the only place that fact exists — every reader
3296
3589
  // downstream sees a check row that looks identical either way (#3716).
3297
3590
  waitExpired: read.settled === false,
3298
- });
3591
+ };
3592
+ // TWO PASSES, AND THE SECOND IS PAID FOR ONLY BY A WAIVER (#3706). The review
3593
+ // surface is read exactly when a waiver is on the table, which is the only
3594
+ // condition whose verdict it can change: `satisfied`, `absent`, `pending`,
3595
+ // `unrecognised` and `undetermined` all reach the same answer without it, and
3596
+ // making them pay a network call would put a new failure mode on paths that
3597
+ // work. Same shape, and the same reason, as the waive-rate sample below.
3598
+ const gate = Object.values(
3599
+ evaluateReviewGate(declaration, read.checks, gateOptions).states
3600
+ ).includes(REVIEW_GATE_STATES.waived)
3601
+ ? evaluateReviewGate(declaration, read.checks, {
3602
+ ...gateOptions,
3603
+ reviewActivity: (options.reviewActivity ?? fetchReviewActivity)(
3604
+ pr,
3605
+ repo
3606
+ ),
3607
+ })
3608
+ : evaluateReviewGate(declaration, read.checks, gateOptions);
3299
3609
  // #3658. Everything above judged ONE commit — this pull request's head — and
3300
3610
  // that is the one commit a batch integration pull request does not speak for.
3301
3611
  // Armed by the same flag as the gate itself, because this IS the gate,
@@ -43,6 +43,35 @@
43
43
  # a gate that reddens every pull request on a vendor's billing state gets
44
44
  # deleted, and then nothing is watching at all.
45
45
  #
46
+ # AND A WAIVER MAY NOT COVER A REVIEW THAT HAPPENED (#3706). Everything above
47
+ # classifies the review check from its `state` and its `description`, and the
48
+ # description is NOT a function of whether a review happened. Measured on
49
+ # CodySwannGT/lisa#3762: `success`, description `Review rate limited`, and the
50
+ # vendor HAD reviewed — it posted `CHANGES_REQUESTED` and found a real defect.
51
+ # The identical string appears on pull requests nothing read, so one string
52
+ # carried two opposite facts and the waiver collapsed them PERMISSIVELY: the one
53
+ # condition this gate exists to let through to a human was the condition it
54
+ # merged. Re-measured on the repository that ships this, 2026-09-06, over the
55
+ # last 40 merged pull requests: 34 reported `Review rate limited`, and 7 of those
56
+ # 34 carried review activity at the head while the published verdict said "this
57
+ # pull request is UNREVIEWED".
58
+ #
59
+ # So the waiver is now CONDITIONAL ON THE ABSENCE OF REVIEW. When a waiver is on
60
+ # the table — and only then, so no other path pays a network call — the guard
61
+ # reads the pull request's own review surface, `reviewDecision` and
62
+ # `reviewThreads`, which is the only surface that answers the question and the
63
+ # one `gh pr checks` cannot see at all. An objection there is `unsatisfied`,
64
+ # never waived. Review activity without an objection stays WAIVED at unchanged
65
+ # severity and merely stops asserting that nobody reviewed. A surface that
66
+ # cannot be read stays WAIVED too, and says the waiver is uncorroborated —
67
+ # refusing there would convert a transient API error into a red gate on a pull
68
+ # request nobody has a finding about.
69
+ #
70
+ # `reviewDecision` rather than the reviews' own states, deliberately: GitHub
71
+ # already computes the latest state per reviewer there, so a DISMISSED objection
72
+ # is not resurrected. Recomputing it here would manufacture the false red
73
+ # tracked on #3720 while closing the false green tracked here.
74
+ #
46
75
  # AND A PULL REQUEST SPEAKS FOR ONE COMMIT ONLY (#3658). Everything above judges
47
76
  # this pull request's HEAD, which is the one commit a batch integration pull
48
77
  # request does not speak for: its head carries the MERGES of every constituent