@codyswann/lisa 3.7.0 → 3.8.1

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 (94) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +21 -12
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/dist/health/ruleset-inspection.d.ts.map +1 -1
  5. package/dist/health/ruleset-inspection.js +106 -1
  6. package/dist/health/ruleset-inspection.js.map +1 -1
  7. package/expo/create-only/.github/required-checks.json +9 -1
  8. package/nestjs/create-only/.github/required-checks.json +9 -1
  9. package/package.json +1 -1
  10. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
  13. package/plugins/lisa/.codex-plugin/skills/lisa-pull-request-review/SKILL.md +50 -2
  14. package/plugins/lisa/rules/eager/falsifiable-checks.md +3 -1
  15. package/plugins/lisa/rules/reference/derived-branch-plan.md +18 -2
  16. package/plugins/lisa/rules/reference/falsifiable-checks.md +57 -5
  17. package/plugins/lisa/scripts/design-source-gate.mjs +11 -2
  18. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
  19. package/plugins/lisa/skills/lisa-pull-request-review/SKILL.md +50 -2
  20. package/plugins/lisa-agy/plugin.json +1 -1
  21. package/plugins/lisa-agy/scripts/design-source-gate.mjs +11 -2
  22. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
  23. package/plugins/lisa-agy/skills/lisa-pull-request-review/SKILL.md +50 -2
  24. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  27. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-copilot/rules/eager/falsifiable-checks.md +3 -1
  31. package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +18 -2
  32. package/plugins/lisa-copilot/rules/reference/falsifiable-checks.md +57 -5
  33. package/plugins/lisa-copilot/scripts/design-source-gate.mjs +11 -2
  34. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
  35. package/plugins/lisa-copilot/skills/lisa-pull-request-review/SKILL.md +50 -2
  36. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +18 -2
  38. package/plugins/lisa-cursor/rules/falsifiable-checks-reference.mdc +57 -5
  39. package/plugins/lisa-cursor/rules/falsifiable-checks.mdc +3 -1
  40. package/plugins/lisa-cursor/scripts/design-source-gate.mjs +11 -2
  41. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
  42. package/plugins/lisa-cursor/skills/lisa-pull-request-review/SKILL.md +50 -2
  43. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-expo-agy/plugin.json +1 -1
  46. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  51. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  56. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  61. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  66. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails-agy/plugin.json +1 -1
  71. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  76. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  81. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/src/base/rules/eager/falsifiable-checks.md +3 -1
  84. package/plugins/src/base/rules/reference/derived-branch-plan.md +18 -2
  85. package/plugins/src/base/rules/reference/falsifiable-checks.md +57 -5
  86. package/plugins/src/base/scripts/design-source-gate.mjs +11 -2
  87. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +29 -1
  88. package/plugins/src/base/skills/lisa-pull-request-review/SKILL.md +50 -2
  89. package/scripts/lisa-github-rulesets.sh +77 -0
  90. package/typescript/copy-overwrite/ast-grep/rules/no-missing-fs-extra-namespace-member-js.yml +71 -0
  91. package/typescript/copy-overwrite/ast-grep/rules/no-missing-fs-extra-namespace-member.yml +90 -0
  92. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +415 -9
  93. package/typescript/create-only/.github/required-checks.json +15 -4
  94. package/typescript/package-lisa/package.lisa.json +2 -1
@@ -85,9 +85,25 @@ Stated plainly, because the session that produced this contract exists to make e
85
85
 
86
86
  This is **not** an executable control. `ready-role-filing`'s sibling guard is a hook that exits 2 and physically refuses a tool call; nothing here refuses anything. What enforces this contract is a validator agent reading `SKILL.md` and choosing to apply it — the same rung of the ladder that failed to bind 13 filings out of 13.
87
87
 
88
- What the regression suite adds is narrower than enforcement and worth naming precisely: it pins the **wording** across all six generated skill roots, so the contract cannot be silently deleted, softened, or lost in a regeneration. It cannot catch a validator that documents the rule and then ignores it at runtime.
88
+ The regression suite has two halves. **Why they bind differently is `falsifiable-checks`' subject, not this rule's** read the quantifier guidance there. What belongs here is the measurement, because this contract happens to be where it was taken.
89
89
 
90
- That is the ceiling for now rather than a shortcut, because `SKILL.md` *is* the validators' execution substrate — there is no compiled artifact underneath to attach a hook to. The honest summary is: absence-as-`false` was undecidable and is now *decidable*, which is a real improvement and a strictly weaker claim than *enforced*. Treat a gate verdict resting on this contract as evidence, not proof, until a deterministic checker exists to read the declaration off a live item.
90
+ - `runtime-behavior-change-decidability.test.ts` asserts the wording is *present* across all six generated skill roots. It pins the contract against silent deletion or a lossy regeneration, and proves nothing about any particular copy.
91
+ - `runtime-behavior-change-consistency.test.ts` asserts no copy says the *wrong* thing, over every occurrence in the corpus: every declaration marker uses the em-dash discriminator, every absence-derivation site resolves to *underivable* within its own sentence, and no phrasing maps an absent section to `false` or hands authority back to the caller.
92
+
93
+ This document states its rule **twice** — once in gate S8, once in the execution step — which is what made the gap observable. `lisa-linear-validate-issue` once spelled the discriminator with an ASCII hyphen in its execution step while its own S8 used the em-dash, and 192 assertions stayed green through that mutation twice running. The fix pinned the execution step and left S8's copy bare. Mutating each site separately, whole suite, before and after the second half landed:
94
+
95
+ | mutated site | before | after |
96
+ |---|---|---|
97
+ | execution step reads an absent section as `false` | 18 | 36 |
98
+ | **gate S8's prose reads an absent section as `false`** | **0** | **18** |
99
+ | writers stop rendering the section | 36 | 54 |
100
+ | a caller's assertion beats the stored declaration | 18 | 36 |
101
+
102
+ Zero out of 11,270 — same file, same rule, same commit, one site pinned and its twin bare.
103
+
104
+ (The wrong spelling is deliberately not written out above. This section sits inside the corpus the consistency suite scans, and the first draft of it quoted the defect as an illustration and failed its own check.)
105
+
106
+ **The executable half of this contract is nothing.** Both suites read markdown. `SKILL.md` *is* the validators' execution substrate — there is no compiled artifact underneath to attach a hook to, so there is no function to call and no return value to assert. What they prove is that the instruction the agent reads is internally consistent; what neither can prove is that the agent then obeys it. Absence-as-`false` was undecidable, is now *decidable* and *non-contradictory*, and both of those are strictly weaker than *enforced*. Treat a gate verdict resting on this contract as evidence, not proof, until a deterministic checker exists to read the declaration off a live item.
91
107
 
92
108
  This costs nothing in derivation difficulty. A single-environment project — Lisa itself is one, `deploy.branches: { production: main }` — recomputes every applicable item to the same branch. The gate was never hard to satisfy; it was impossible to *audit*, and those are different problems with different fixes. Weakening S19 would have fixed neither.
93
109
 
@@ -63,17 +63,69 @@ A clean result is a statement about what the check can perceive, not about the c
63
63
 
64
64
  1. Author the check.
65
65
  2. Deliberately break the guarded property — specifically, introduce **the exact regression the guard exists to prevent**, not a nearby or convenient break.
66
- 3. Confirm the check **fails, names the right file/line, and that exactly one test fails**. A failure that does not localize is weak evidence the check is measuring the right thing.
66
+ 3. Confirm the check **fails and names the right file/line**, then read the failure count and the failing test *names* per the yardstick below.
67
67
  4. Restore, and confirm green again.
68
68
  5. Report the falsification alongside the result.
69
69
 
70
70
  ### The failure count is part of the evidence
71
71
 
72
- Step 3's cardinality is not pedantry; each deviation names a distinct defect.
72
+ Step 3's cardinality is not pedantry; each deviation names a distinct defect. Run it as four steps, in this order:
73
73
 
74
- - **Zero failures** the guard is inert. It was authored, it is green, and it asserts nothing about the property it names. The originating incident: a guard pinned one field of a structure and the regression it was written to stop walked straight through it with all fifty tests green, because nothing in the suite touched the field that actually moved.
75
- - **More than one failure** — the break is too coarse, the guard is over-broad, or unrelated tests share the fixture. Any of the three means the guard's next real failure will not tell the reader what broke, which is most of a guard's value.
76
- - **Exactly one failure** the guard localizes. That is the evidence, and it is what gets reported.
74
+ 1. **Neuter the protection in production code** delete the branch, drop the token from the enum, remove the check. Editing a *test* proves nothing about the guard; it only proves the test file is loaded.
75
+ 2. **Run the whole suite.** Never a single test file, and never a path filter.
76
+ 3. **Count the failures and read their names.** The count alone is ambiguous; the names are what separate "several failures of one regression" from "several unrelated failures."
77
+ 4. **Revert, and confirm green again.**
78
+
79
+ Then read the result:
80
+
81
+ - **Zero failures** — the guard is inert. It was authored, it is green, and it asserts nothing about the property it names. Two real instances: a guard pinned one field of a structure and the regression it was written to stop walked through it with all fifty tests green; and removing either `conflicting` or `unreadable` from `design-source-gate.mjs`'s `VIOLATION_STATUSES` failed **0 of 38 tests** while flipping the gate's verdict from FAIL to PASS.
82
+ - **Many failures, unrelated to each other** — the break is too coarse, the guard is over-broad, or unrelated tests share a fixture. Any of the three means the guard's next real failure will not tell the reader what broke, which is most of a guard's value.
83
+ - **Exactly one failure, or several all named for the same regression** — the guard localizes. Both readings are load-bearing and correctly scoped. Removing the two-token `--config-env` check failed **3** tests and pinning an index failed **5**; every failing name in both runs described the removed behaviour, so both guards were correct. This is a reading exercise, not arithmetic — "more than one is bad" is the wrong rule.
84
+
85
+ ### A zero is robust to contention; a positive count is not
86
+
87
+ The obvious objection to any cardinality measured on a shared machine — *"your probe ran on a loaded box, so how do you trust the number?"* — has an asymmetric answer, and the asymmetry is worth stating because it decides which numbers you must re-measure.
88
+
89
+ **Load can only add failures, never remove them.** A test that passes under contention would also have passed on a quiet machine; contention causes timeouts and lock races, which turn green into red, never red into green. So:
90
+
91
+ - **A zero stands regardless of what else was running.** If the whole suite reports zero failures attributable to your mutation on a loaded box, a quiet box cannot produce fewer. `0 of 11,270` is `0 of 11,270`.
92
+ - **A non-zero count needs a quiet machine**, because contention inflates it. Failures that are really flakes get miscounted as the guard's, which reads as "over-broad" and gets a correctly-scoped guard deleted.
93
+
94
+ Practical consequence: an inert-guard finding is safe to report from a busy machine, while the "exactly one, or several all named alike" reading is only trustworthy once you have separated your mutation's failures from the load-flake population — which is what reading the *names* is for.
95
+
96
+ ### Two reasons a real protection reports zero
97
+
98
+ A cardinality of zero has two distinct innocent causes, and they need different fixes. Rule out both before concluding a guard is inert:
99
+
100
+ 1. **Wrong probe scope** — the probe was scoped to a filename or path and is blind to a split suite. Fix: run the whole suite.
101
+ 2. **Wrong quantifier** — the assertions are existential over a corpus that contains duplicates, so a correct copy satisfies them on the broken copy's behalf. Fix: assert the universal negative.
102
+
103
+ ### Never scope the probe by filename
104
+
105
+ **A cardinality probe scoped to a filename reports zero when a suite splits. Follow the protection, not the path.**
106
+
107
+ Observed live: a reviewer neutered `before_end_of_options` (an end-of-options security fix), grepped `block-direct-issue-create.test.ts` for coverage, found none, and reported **cardinality 0 — the fix ships untested**. It was relayed as fact and used to question the author's discipline. Re-measuring against the whole suite showed neutering the protection fails **4** tests, each named for it: the suite had split at the 300-effective-line lint ceiling, and the declaration arm — including all five end-of-options tests — had moved to `block-direct-issue-create-declarations.test.ts`.
108
+
109
+ The false negative fires exactly where this repo is most likely to split a file: at the line cap, on the suites that have grown *because* someone added protections to them. So:
110
+
111
+ - Never scope the probe by filename or path filter.
112
+ - Never infer absence of coverage from a grep. A grep locates text; it cannot enumerate what a mutation breaks.
113
+ - A prior cardinality-0 finding measured with a path-scoped probe must be **re-measured** with a whole-suite probe before anyone acts on it.
114
+
115
+ ### Assert that no copy is wrong, not that some copy is right
116
+
117
+ A test asserting that the **correct** marker is *present* passes trivially when the corpus happens to state it twice — the second statement satisfies the assertion no matter what the first one says, so the test survives the exact edit it exists to catch. The falsifiable form asserts the **wrong** spelling is *absent*: it has no second copy to fall back on, and it goes red the moment the wrong form reappears.
118
+
119
+ Stated as quantifiers, which is the general form:
120
+
121
+ - **Existential** — *"some copy says the right thing"* — is trivially satisfied by any other copy. A wrong copy is undetectable.
122
+ - **Universal negative** — *"no copy says the wrong thing"* — cannot be satisfied by a correct copy elsewhere.
123
+
124
+ Observed: a suite over a corpus containing two copies of the same rule returned **cardinality 0** on a real mutation because every assertion was existential. Rewriting the assertions to universal negatives — same code, same corpus, only the quantifier changed — took the same mutation from **0 to 18** failures. The defect it had been hiding: one PR pinned the *execution-step* copy of a rule and left the *gate* copy entirely unpinned, so reverting the gate's "absent section means underivable — never `false`" back to "means `false`" failed **0 of 11,270 tests** while the validator's documented gate contradicted its own execution step.
125
+
126
+ ### Enumerate the property, not the known-bad instance
127
+
128
+ A universal negative against one hard-coded wrong spelling still only sees that spelling. Assert the **property** instead: *every* marker uses an em-dash catches the en-dash and the missing dash too, which `not.toContain("<one wrong spelling>")` never would. When that generalisation was made, it went red immediately on a wrong marker its own author had just written into rule prose — the discipline catching the person applying it, in the same run.
77
129
 
78
130
  ### Never narrate a red state you did not run
79
131
 
@@ -294,8 +294,17 @@ export function classifyDesignSource(file, config) {
294
294
  return { ...base, status: "undeclared" };
295
295
  }
296
296
 
297
- /** Per-file statuses that fail the gate. */
298
- const VIOLATION_STATUSES = new Set([
297
+ /**
298
+ * Per-file statuses that fail the gate.
299
+ *
300
+ * Exported so the suite can pin the set itself, not just its members' behavior.
301
+ * Classifying a file `unreadable` is not a guard; *failing the change* because
302
+ * of it is. Issue #2492 measured the difference: with only classification
303
+ * asserted, deleting `conflicting` or `unreadable` from this list flipped the
304
+ * verdict to PASS with the whole suite still green. The pinning test now fails
305
+ * on any edit to this set — a removal and an addition alike.
306
+ */
307
+ export const VIOLATION_STATUSES = new Set([
299
308
  "undeclared",
300
309
  "malformed",
301
310
  "conflicting",
@@ -291,7 +291,22 @@ Delegate to the `pull-request-review` skill with the PR number. It owns the whol
291
291
  comment cycle: fetch every unresolved human + bot thread (with resolution state via
292
292
  GraphQL), implement valid feedback (commit + push), reply to invalid feedback, and
293
293
  resolve every thread via `resolveReviewThread` so the branch-protection
294
- thread-resolution gate clears. If that skill needs to push a commit, leave
294
+ thread-resolution gate clears.
295
+
296
+ **A green review check is not proof a review happened.** That skill's Step 1b
297
+ returns a `reviewed` / `NOT REVIEWED` verdict — record it, and repeat it in this
298
+ skill's final report. Measured (CodySwannGT/lisa#2497): `CodeRabbit` was a
299
+ *required* context and posted `success — "Review rate limited"` on #2483 and
300
+ #2484, so branch protection recorded a satisfied review gate for two
301
+ security-relevant PRs nothing had read; both merged and shipped in `v3.5.1`.
302
+
303
+ `NOT REVIEWED` is **not a blocker** — do not hold the merge on it, do not treat
304
+ it as a failing check, and do not try to force the bot to re-run. A hollow
305
+ review check is usually an org-wide vendor spending cap, which is a billing
306
+ matter no amount of driving will clear, and whether such a check belongs in the
307
+ required set at all is an open owner decision. It is a **reporting** obligation:
308
+ the PR merged unreviewed, and the report has to say so instead of implying a
309
+ review it did not get. If that skill needs to push a commit, leave
295
310
  auto-merge armed (section 1); when it returns, re-read `headRefOid` and reset
296
311
  `verify_commit` to the returned/pushed head, then continue. Do not re-implement review handling here
297
312
  — it is the single source of truth for review-thread handling.
@@ -452,3 +467,16 @@ At every terminal state, release the babysitter lease
452
467
  (`gh pr edit <pr> --remove-label "lisa:babysitter-on-duty"`) so the CI
453
468
  auto-fix workflow can take over as fixer of last resort if the branch goes
454
469
  red later with nobody driving it.
470
+
471
+ **Every terminal report carries the step-(d) review verdict**, including a
472
+ successful `MERGED`. "Merged, all checks green" is exactly the sentence that hid
473
+ #2483 and #2484: both were green, both were merged, and neither had been read by
474
+ anything. Green means *no gate objected*; it does not mean *something looked*.
475
+ So state the verdict alongside the outcome:
476
+
477
+ - `MERGED — reviewed (CodeRabbit "Review approved")`
478
+ - `MERGED — NOT REVIEWED: CodeRabbit posted success but "Review rate limited"`
479
+
480
+ This is reporting, never a terminal state of its own. `NOT REVIEWED` does not
481
+ turn a merged PR into a blocked one, and it must never be used to withhold a
482
+ merge — it changes what the record says, not what the loop does.
@@ -37,8 +37,49 @@ gh api graphql -f query='
37
37
  -F owner=<owner> -F repo=<repo> -F pr=<pr>
38
38
  ```
39
39
 
40
- Keep only threads where `isResolved == false`. If there are none, report success
41
- and exit (nothing to do).
40
+ Keep only threads where `isResolved == false`.
41
+
42
+ **If there are none, you have not yet learned anything — run Step 1b before
43
+ concluding.** Zero unresolved threads has two completely different causes, and
44
+ this query cannot tell them apart: *a reviewer looked and found nothing*, or
45
+ *nothing ever looked*. Reporting the first when it was the second is the defect
46
+ in CodySwannGT/lisa#2497.
47
+
48
+ ## Step 1b: Did any review actually happen? (required before reporting)
49
+
50
+ A required review check can post `success` having reviewed nothing. Measured on
51
+ PRs #2483 and #2484: `CodeRabbit` reported `success — "Review rate limited"`,
52
+ zero reviews, and both merged on that green carrying security-relevant changes.
53
+
54
+ ```bash
55
+ gh pr checks <pr> --json name,state,bucket,description \
56
+ --jq '.[] | select(.name | test("(?i)coderabbit|review")) | "\(.name)\t\(.state)\t\(.description)"'
57
+ ```
58
+
59
+ **The state column reads `SUCCESS` whether the review was real or hollow — only
60
+ the description distinguishes them.** `Review approved` / `Review completed` is
61
+ a real review; `Review rate limited`, `Review queued`, or a missing context is
62
+ not. Never read `gh pr view --json statusCheckRollup` for this: CodeRabbit posts
63
+ a legacy commit status, which that route returns *without* the description.
64
+
65
+ Where the project ships Lisa's guard, prefer its machine-readable form, which
66
+ also says whether the check is ruleset-required (so whether branch protection
67
+ recorded a satisfied review gate for a review that did not happen):
68
+
69
+ ```bash
70
+ node scripts/check-skipped-required-checks.mjs --pr=<pr> --json
71
+ ```
72
+
73
+ It reports and never fails — a hollow check is often a vendor spending cap, not
74
+ a repository defect, and it is not this skill's call to block on one.
75
+
76
+ Carry the finding into your Step 4 report. **Do not write "no unresolved review
77
+ threads" on its own** — it is true of an unreviewed PR too. Say which you
78
+ observed:
79
+
80
+ - `reviewed — CodeRabbit "Review approved", 0 unresolved threads`
81
+ - `NOT REVIEWED — CodeRabbit success but "Review rate limited" (vacuous); 0 threads means nobody looked`
82
+ - `NOT REVIEWED — no review check reported on this PR at all`
42
83
 
43
84
  ## Step 2: Triage and act on each unresolved thread
44
85
 
@@ -75,6 +116,13 @@ judgment. This skill resolves **threads**; it does not dismiss review-decision
75
116
  gates (`CHANGES_REQUESTED`) or merge the PR — the caller (`drive-pr-to-merge`)
76
117
  owns those.
77
118
 
119
+ **Open the report with the Step 1b verdict, before the thread counts.** A thread
120
+ summary describes what was done about review findings; it says nothing about
121
+ whether a review produced any. State `reviewed` or `NOT REVIEWED (<why>)` first,
122
+ then the counts. A caller that records "reviews addressed" in evidence must
123
+ carry that verdict through verbatim — a PR whose only review check was vacuous
124
+ has not been reviewed, no matter how clean its thread list is.
125
+
78
126
  ## Composition
79
127
 
80
128
  - **Standalone**: `/lisa:pull-request:review <pr>`.
@@ -351,6 +351,82 @@ strip_config_dropped_checks() {
351
351
  else . end'
352
352
  }
353
353
 
354
+ # Per-repo opt-IN, the mirror of dropRequiredChecks above. Some repositories run
355
+ # a high-signal check that only exists in THAT repository, so it cannot live in
356
+ # a shared template: putting it there would ship a context host projects never
357
+ # report, and a required check that never reports blocks every pull request
358
+ # forever (the #2476 "aspirational seed a guard then trusts" defect).
359
+ #
360
+ # Keyed by ruleset name, because more than one shipped template carries a
361
+ # required_status_checks rule (`base` and `quality checks` both do) and an
362
+ # unkeyed list could not say which one it meant:
363
+ # { "github": { "rulesets": { "addRequiredChecks": {
364
+ # "quality checks": [
365
+ # { "context": "🧩 Plugin artifacts match source", "integration_id": 15368 }
366
+ # ] } } } }
367
+ #
368
+ # `integration_id` is optional and defaults to GitHub Actions. Contexts already
369
+ # present are not duplicated, and a ruleset with no required_status_checks rule
370
+ # gets one created so an addition is never silently dropped.
371
+ #
372
+ # Additions are applied FIRST, before the no-workflows strip and before
373
+ # dropRequiredChecks, so both of those still win over an addition. That is
374
+ # deliberate: the no-workflows strip is a safety rule (a required Actions check
375
+ # on a repository with no workflows can never report, and would block every pull
376
+ # request), and naming the same context in both lists is operator error whose
377
+ # safe resolution is to drop it rather than to require it.
378
+ add_config_required_checks() {
379
+ local json="$1"
380
+ local project_path="$2"
381
+ local ruleset_name="$3"
382
+ local config="$project_path/.lisa.config.json"
383
+
384
+ if [[ ! -f "$config" ]]; then
385
+ echo "$json"
386
+ return 0
387
+ fi
388
+
389
+ local added
390
+ if ! added=$(jq -c --arg name "$ruleset_name" \
391
+ '.github.rulesets.addRequiredChecks[$name] // []' "$config" 2>/dev/null); then
392
+ log_warning ".lisa.config.json could not be parsed — ignoring github.rulesets overrides" >&2
393
+ added="[]"
394
+ fi
395
+
396
+ if [[ "$added" == "[]" || "$added" == "null" ]]; then
397
+ echo "$json"
398
+ return 0
399
+ fi
400
+
401
+ echo "$json" | jq --argjson added "$added" --argjson actions "$ACTIONS_INTEGRATION_ID" '
402
+ ($added
403
+ | map(select(type == "object" and (.context | type) == "string"))
404
+ | map({ context: .context, integration_id: (.integration_id // $actions) })
405
+ ) as $checks
406
+ | if ($checks | length) == 0 then .
407
+ elif ((.rules // []) | map(select(.type == "required_status_checks")) | length) > 0 then
408
+ .rules |= map(
409
+ if .type == "required_status_checks" then
410
+ .parameters.required_status_checks |= (
411
+ . as $existing
412
+ | . + ($checks | map(
413
+ select(.context as $c | ($existing | map(.context) | index($c)) | not)
414
+ ))
415
+ )
416
+ else . end
417
+ )
418
+ else
419
+ .rules = ((.rules // []) + [{
420
+ type: "required_status_checks",
421
+ parameters: {
422
+ strict_required_status_checks_policy: false,
423
+ do_not_enforce_on_create: true,
424
+ required_status_checks: $checks
425
+ }
426
+ }])
427
+ end'
428
+ }
429
+
354
430
  apply_ruleset() {
355
431
  local repo="$1"
356
432
  local template_file="$2"
@@ -365,6 +441,7 @@ apply_ruleset() {
365
441
 
366
442
  local clean_template
367
443
  clean_template=$(strip_readonly_fields "$template_content")
444
+ clean_template=$(add_config_required_checks "$clean_template" "$project_path" "$ruleset_name")
368
445
  clean_template=$(strip_actions_checks_if_no_workflows "$clean_template" "$project_path")
369
446
  clean_template=$(strip_config_dropped_checks "$clean_template" "$project_path")
370
447
 
@@ -0,0 +1,71 @@
1
+ # This file is managed by Lisa.
2
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
3
+ #
4
+ # JavaScript-language twin of `no-missing-fs-extra-namespace-member.yml`.
5
+ #
6
+ # ast-grep binds one parser per rule: `language: typescript` scans only
7
+ # .ts/.mts/.cts, so .js/.mjs/.cjs would otherwise be an unguarded gap — and a
8
+ # guard with a silent hole is the failure mode this rule exists to prevent.
9
+ # Build scripts and tooling in TypeScript projects are routinely .mjs, and
10
+ # fs-extra is exactly the kind of dependency they reach for.
11
+ #
12
+ # The `constraints` block MUST stay byte-identical to the TypeScript rule.
13
+ # `tests/unit/config/ast-grep-template.test.ts` asserts both files carry the
14
+ # same allow list AND that it still equals the set real Node reports, so these
15
+ # two cannot drift apart or drift away from reality unnoticed.
16
+ #
17
+ # See the TypeScript rule for the full rationale on why this is an allow list
18
+ # rather than a deny list, and why the set is stable across fs-extra 11.x.
19
+ id: no-missing-fs-extra-namespace-member-js
20
+ language: javascript
21
+ severity: error
22
+ message: |
23
+ `fs-extra` does not expose this member on its ESM namespace object.
24
+
25
+ `import * as fse from "fs-extra"` resolves to fs-extra's CommonJS entry, so
26
+ Node derives the namespace keys with cjs-module-lexer. Only fs-extra's own
27
+ directly-assigned functions survive that. Absent members include every JSON
28
+ helper (readJson, readJSON, writeJson, outputJson, and their Sync forms),
29
+ every node:fs passthrough (readFile, writeFile, readdir, stat, existsSync,
30
+ appendFile, ...), and copySync, moveSync, removeSync, pathExistsSync and
31
+ outputFileSync.
32
+
33
+ The call is `undefined` at run time and throws a TypeError, and unit tests
34
+ cannot see it — Vitest and Jest resolve the import through CJS interop and
35
+ hand back the default export's properties.
36
+
37
+ Fix by importing the concrete API instead:
38
+ - file I/O -> `readFile` / `writeFile` from `node:fs/promises`
39
+ - JSON -> read the file, then `JSON.parse` / `JSON.stringify`
40
+ - whole module -> `import fse from "fs-extra"`. The DEFAULT export does
41
+ carry every member and is deliberately not flagged here.
42
+ # Test trees are excluded on purpose — see the TypeScript rule for the full
43
+ # rationale. In short: the defect class is "works under a bundler, `undefined`
44
+ # under real Node", so it can only bite code Node executes directly, and the
45
+ # runtime control in the Lisa monorepo already scopes itself to source trees
46
+ # for exactly that reason. Keep this list in step with the TypeScript rule.
47
+ ignores:
48
+ - '**/tests/**'
49
+ - '**/test/**'
50
+ - '**/__tests__/**'
51
+ - '**/__mocks__/**'
52
+ - '**/*.test.js'
53
+ - '**/*.test.mjs'
54
+ - '**/*.test.cjs'
55
+ - '**/*.test.jsx'
56
+ - '**/*.spec.js'
57
+ - '**/*.spec.mjs'
58
+ - '**/*.spec.cjs'
59
+ - '**/*.spec.jsx'
60
+ rule:
61
+ all:
62
+ - pattern: $NS.$MEMBER
63
+ - inside:
64
+ kind: program
65
+ stopBy: end
66
+ has:
67
+ pattern: import * as $NS from "fs-extra"
68
+ constraints:
69
+ MEMBER:
70
+ not:
71
+ regex: ^(copy|createFile|createFileSync|createLink|createLinkSync|createSymlink|createSymlinkSync|default|emptyDir|emptyDirSync|emptydir|emptydirSync|ensureDir|ensureDirSync|ensureFile|ensureFileSync|ensureLink|ensureLinkSync|ensureSymlink|ensureSymlinkSync|exists|mkdirp|mkdirpSync|mkdirs|mkdirsSync|move|outputFile|pathExists|read|readv|remove|write|writev)$
@@ -0,0 +1,90 @@
1
+ # This file is managed by Lisa.
2
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
3
+ #
4
+ # Bans `fs-extra` namespace members that do not exist under real Node ESM.
5
+ #
6
+ # The allow list below is an ALLOW list, not a deny list, and that is
7
+ # deliberate. A deny list fails open: any absent member nobody thought to
8
+ # enumerate passes silently, which is the exact rot this rule exists to
9
+ # prevent. An allow list fails closed — an unrecognised member is flagged
10
+ # loudly and a human decides.
11
+ #
12
+ # The list is the set of keys real Node reports for
13
+ # `import * as fse from "fs-extra"`. It is byte-identical across fs-extra
14
+ # 11.1.1, 11.3.3, 11.3.4 and 11.3.5, because the bare specifier always routes
15
+ # through the package `exports` map to the CommonJS entry (`./lib/index.js`)
16
+ # and Node derives the namespace keys with cjs-module-lexer from a re-export
17
+ # shape that has not changed. `lib/esm.mjs` — which does export the JSON
18
+ # helpers — is reachable only via the `fs-extra/esm` subpath, which is a
19
+ # different import.
20
+ #
21
+ # Anti-rot: `tests/unit/core/fs-extra-namespace-callsites.test.ts` in the Lisa
22
+ # monorepo derives this same set from real Node at run time, and
23
+ # `tests/unit/config/ast-grep-template.test.ts` asserts the list below still
24
+ # equals it. If fs-extra ever changes its namespace surface, that pairing goes
25
+ # red centrally instead of this list silently drifting in every host project.
26
+ id: no-missing-fs-extra-namespace-member
27
+ language: typescript
28
+ severity: error
29
+ message: |
30
+ `fs-extra` does not expose this member on its ESM namespace object.
31
+
32
+ `import * as fse from "fs-extra"` resolves to fs-extra's CommonJS entry, so
33
+ Node derives the namespace keys with cjs-module-lexer. Only fs-extra's own
34
+ directly-assigned functions survive that. Absent members include every JSON
35
+ helper (readJson, readJSON, writeJson, outputJson, and their Sync forms),
36
+ every node:fs passthrough (readFile, writeFile, readdir, stat, existsSync,
37
+ appendFile, ...), and copySync, moveSync, removeSync, pathExistsSync and
38
+ outputFileSync.
39
+
40
+ The call is `undefined` at run time and throws a TypeError. Unit tests are
41
+ structurally blind to it: Vitest and Jest resolve the import through CJS
42
+ interop and hand back the default export's properties, so the call is a
43
+ function in every test and `undefined` in production. Lisa shipped this
44
+ defect three times in one release window (#2482, #2487) — once as a crash
45
+ that stranded seven merged PRs, twice inside error-swallowing `catch` blocks
46
+ where the TypeError replaced the error being handled.
47
+
48
+ Fix by importing the concrete API instead:
49
+ - file I/O -> `readFile` / `writeFile` from `node:fs/promises`
50
+ - JSON -> read the file, then `JSON.parse` / `JSON.stringify`
51
+ - whole module -> `import fse from "fs-extra"`. The DEFAULT export does
52
+ carry every member and is deliberately not flagged here.
53
+ # Test trees are excluded on purpose, matching the scope the runtime control
54
+ # already chose. The defect class is "works under a bundler, `undefined` under
55
+ # real Node" — so it can only bite code that Node executes directly. Vitest and
56
+ # Jest resolve `import * as fse from "fs-extra"` through CJS interop and hand
57
+ # back the default export's properties, which is precisely why test files can
58
+ # call `fse.writeJson` forever without ever being wrong.
59
+ #
60
+ # `tests/unit/core/fs-extra-namespace-callsites.test.ts` scopes itself to
61
+ # ["src", "scripts"] for this reason — "trees whose code is executed by Node
62
+ # directly, not through a bundler". This rule matches that scope rather than
63
+ # inventing a second, wider one. Path globs are used instead of a src/scripts
64
+ # allowlist because host project layouts vary (apps/, packages/, lib/) while
65
+ # test directory conventions do not.
66
+ ignores:
67
+ - '**/tests/**'
68
+ - '**/test/**'
69
+ - '**/__tests__/**'
70
+ - '**/__mocks__/**'
71
+ - '**/*.test.ts'
72
+ - '**/*.test.mts'
73
+ - '**/*.test.cts'
74
+ - '**/*.test.tsx'
75
+ - '**/*.spec.ts'
76
+ - '**/*.spec.mts'
77
+ - '**/*.spec.cts'
78
+ - '**/*.spec.tsx'
79
+ rule:
80
+ all:
81
+ - pattern: $NS.$MEMBER
82
+ - inside:
83
+ kind: program
84
+ stopBy: end
85
+ has:
86
+ pattern: import * as $NS from "fs-extra"
87
+ constraints:
88
+ MEMBER:
89
+ not:
90
+ regex: ^(copy|createFile|createFileSync|createLink|createLinkSync|createSymlink|createSymlinkSync|default|emptyDir|emptyDirSync|emptydir|emptydirSync|ensureDir|ensureDirSync|ensureFile|ensureFileSync|ensureLink|ensureLinkSync|ensureSymlink|ensureSymlinkSync|exists|mkdirp|mkdirpSync|mkdirs|mkdirsSync|move|outputFile|pathExists|read|readv|remove|write|writev)$