@codyswann/lisa 4.30.0 → 4.31.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 (66) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +4 -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 +8 -9
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/expo/create-only/.github/required-checks.json +1 -1
  9. package/nestjs/create-only/.github/required-checks.json +1 -1
  10. package/package.json +4 -4
  11. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  17. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo-agy/plugin.json +1 -1
  24. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  44. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails-agy/plugin.json +1 -1
  49. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  54. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  59. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  61. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +86 -143
  62. package/typescript/create-only/.github/required-checks.json +1 -1
  63. package/typescript/create-only/.github/workflows/review-evidence.yml +1 -1
  64. package/typescript/deletions.json +2 -1
  65. package/typescript/package-lisa/package.lisa.json +2 -2
  66. package/typescript/create-only/.github/workflows/required-checks-drift.yml +0 -91
@@ -12,7 +12,7 @@
12
12
  * yours to edit).
13
13
  *
14
14
  * Usage:
15
- * node scripts/check-skipped-required-checks.mjs [rootDir] [--remote] [--json]
15
+ * node scripts/check-skipped-required-checks.mjs [rootDir] [--json]
16
16
  * node scripts/check-skipped-required-checks.mjs --vacuity [--fail-on-vacuous]
17
17
  * node scripts/check-skipped-required-checks.mjs --pr=1234 [--repo=OWNER/NAME]
18
18
  *
@@ -31,7 +31,7 @@
31
31
  *
32
32
  * Two of the three live here:
33
33
  *
34
- * - **Skipped** (`--remote` / offline arm, below): GitHub counts a `skipped`
34
+ * - **Skipped** (the offline arm, below): GitHub counts a `skipped`
35
35
  * required check as SATISFIED, so a `skip_jobs` token makes the gate
36
36
  * decorative. Static, offline, BLOCKING.
37
37
  * - **Vacuous** (`--pr` arm): the check really ran and really reported
@@ -40,11 +40,15 @@
40
40
  *
41
41
  * ## Where this runs
42
42
  *
43
- * Lisa's `quality.yml` runs the OFFLINE arm on every pull request, and the
44
- * shipped `required-checks-drift.yml` runs `--remote` on a weekly schedule.
45
- * That split is deliberate and is argued under "Why this is a DECLARATION
46
- * guard" below: the enforced PR path may not depend on network or `gh` auth,
47
- * and the snapshot it enforces may not be allowed to rot unwatched.
43
+ * Lisa's `quality.yml` runs this on every pull request. EVERY arm is offline:
44
+ * nothing here reads a live ruleset, needs a token, or touches the network for
45
+ * the required-context rules. That is a deliberate reduction (#3599) — the
46
+ * scheduled arm that used to re-read the ruleset was removed along with the
47
+ * `administration:read` credential it required, which sat as a standing repo
48
+ * secret in every consumer to detect a rare event. If the committed snapshot
49
+ * and the live ruleset drift apart, that is now discovered by consequence
50
+ * rather than by check, and that is the accepted trade. Do not reintroduce a
51
+ * cheaper detector for it.
48
52
  *
49
53
  * ## `enforcement`
50
54
  *
@@ -73,13 +77,26 @@
73
77
  * assert.
74
78
  *
75
79
  * So `required_contexts` is treated as a cache of a live fetch, not as
76
- * testimony. It is trusted only while `ruleset.baseline_fetched_at` carries a
77
- * parseable timestamp no older than SNAPSHOT_MAX_AGE_DAYS. Untranscribed or
78
- * expired, the guard REFUSES to answer: every rule that depends on
79
- * `required_contexts` is skipped, no ✅ is printed, and the report says NOT
80
- * CHECKED and why. The rules that do NOT read `required_contexts` — an
81
- * undeclared token, a token written with whitespace — still run, because they
82
- * are true regardless of what the ruleset requires.
80
+ * testimony. Trust is a function of PRESENCE: it is believed while
81
+ * `ruleset.baseline_fetched_at` carries a parseable timestamp, and refused when
82
+ * that stamp is empty or unreadable. Untranscribed, the guard REFUSES to
83
+ * answer: every rule that depends on `required_contexts` is skipped, no ✅ is
84
+ * printed, and the report says NOT CHECKED and why. The rules that do NOT read
85
+ * `required_contexts` — an undeclared token, a token written with whitespace —
86
+ * still run, because they are true regardless of what the ruleset requires.
87
+ *
88
+ * NOTHING EXPIRES, and that is a decided constraint (#3599), not an oversight.
89
+ * A ninety-day ceiling used to sit here, and it was coherent only while a
90
+ * scheduled arm existed that could re-read the ruleset and re-stamp the file.
91
+ * With that arm removed, a deadline is an obligation with no way to discharge
92
+ * it: somebody would have to re-transcribe every consumer's snapshot every
93
+ * quarter forever, or the guard silently goes NOT CHECKED. A guard that
94
+ * reliably expires into "not checked" is worse than one answering from a stamp
95
+ * somebody wrote once, because the first looks healthy right up until it has
96
+ * been quietly inert for a quarter. A softer form — believe it, but warn past N
97
+ * days — is the same substitution and is refused for the same reason: it
98
+ * recreates the recurring manual obligation and adds a signal nobody can act
99
+ * on. Non-empty believed. Empty refused. Nothing expiring.
83
100
  *
84
101
  * Refusal is exit 1, so an explicit `npm run check:skipped-required-checks`
85
102
  * cannot be mistaken for a pass. Under `"enforcement": "warn"` — which is what
@@ -114,16 +131,18 @@
114
131
  * authoritative alone; the coherence rules below are what stop either rotting
115
132
  * into decoration.
116
133
  *
117
- * That mutual policing has one blind spot, and `--remote` exists for it: two
118
- * snapshots in one repo can only catch each other drifting from the CODE.
134
+ * That mutual policing has one blind spot, and it is now UNCOVERED ON PURPOSE:
135
+ * two snapshots in one repo can only catch each other drifting from the CODE.
119
136
  * Neither can see the ruleset itself change in the admin console — which is
120
- * exactly how acmeorgd's list silently went from ten contexts to eleven, with every
121
- * test still green, because the "independent" transcription was made from the
122
- * same reading at the same moment.
137
+ * exactly how one repository's list silently went from ten contexts to eleven,
138
+ * with every test still green, because the "independent" transcription was made
139
+ * from the same reading at the same moment.
123
140
  *
124
- * `--remote` is opt-in so the ENFORCED path stays offline. A guard that needed
125
- * network and `gh` auth on every run would flake, and a flaky guard gets
126
- * skipped — which reintroduces exactly the false-green class this file refuses.
141
+ * A scheduled arm used to close that gap by reading the live ruleset, and it
142
+ * was removed (#3599) because closing it cost a permanent `administration:read`
143
+ * token in every consumer. The blind spot is the price of not holding that
144
+ * credential. It is stated here rather than hidden, and it is not an invitation
145
+ * to build a lighter-weight version of the same thing.
127
146
  *
128
147
  * ## `--pr` — the VACUOUS arm, and why it only ever reports
129
148
  *
@@ -148,8 +167,8 @@
148
167
  * ## `--vacuity` — why a flag nobody passes is the same defect
149
168
  *
150
169
  * MEASURED (CodySwannGT/lisa#2928): for the whole life of this arm, NOTHING
151
- * INVOKED IT. `quality.yml` ran the offline arm with no `--pr`, the shipped
152
- * `required-checks-drift.yml` ran `--remote`, and the package script named
170
+ * INVOKED IT. `quality.yml` ran the offline arm with no `--pr`, the then-shipped
171
+ * scheduled drift workflow ran its own arm, and the package script named
153
172
  * `check:vacuous-required-checks` was a bare invocation — so the command named
154
173
  * for the vacuous check reported on SKIPS and said nothing about vacuity unless
155
174
  * a caller happened to remember `-- --pr=1234`. A rule that runs only when
@@ -270,18 +289,6 @@ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
270
289
  /** Repo-relative path of the per-repo declaration file. */
271
290
  export const DECLARATION_PATH = ".github/required-checks.json";
272
291
 
273
- /**
274
- * How long a transcribed `required_contexts` snapshot stays trustworthy.
275
- *
276
- * A SOURCE CONSTANT, not an input: a ceiling somebody can widen from a config
277
- * file fails open on exactly the runs nobody tests. Rulesets are edited in an
278
- * admin console with no signal in the repository, so an old transcription is
279
- * not evidence — it is a memory of evidence. Ninety days is long enough that
280
- * the weekly `--remote` run refreshes it many times over, and short enough that
281
- * an abandoned repository stops being told its skips are fine.
282
- */
283
- export const SNAPSHOT_MAX_AGE_DAYS = 90;
284
-
285
292
  /**
286
293
  * Matches a `skip_jobs:` key and captures everything after the colon.
287
294
  *
@@ -331,7 +338,6 @@ export const VIOLATIONS = Object.freeze({
331
338
  stale: "declaration_overstates_requirement",
332
339
  orphaned: "orphaned_exemption",
333
340
  badExemption: "exemption_without_valid_ticket",
334
- remoteDrift: "ruleset_snapshot_drift",
335
341
  whitespace: "whitespace_in_skip_token",
336
342
  vacuous: "vacuous_required_check",
337
343
  unproven: "unproven_required_check",
@@ -399,15 +405,8 @@ const ENFORCEMENT_MODES = Object.freeze(["error", "warn"]);
399
405
  * both that a context is ruleset-required AND that a token it actually skips
400
406
  * silences it; that is a reviewed state, and shipping past it is the exact
401
407
  * defect this file exists to refuse.
402
- *
403
- * `ruleset_snapshot_drift` blocks for the same reason: it only fires under an
404
- * explicit `--remote` on a repository that filled in its ruleset ids, and it is
405
- * measured against live truth rather than inferred from a seed.
406
408
  */
407
- const ALWAYS_BLOCKING = Object.freeze([
408
- VIOLATIONS.suppressesRequired,
409
- VIOLATIONS.remoteDrift,
410
- ]);
409
+ const ALWAYS_BLOCKING = Object.freeze([VIOLATIONS.suppressesRequired]);
411
410
 
412
411
  /**
413
412
  * Violation kinds that NEVER fail the build, in any enforcement mode.
@@ -488,6 +487,27 @@ export const SETTLE_INTERVAL_SECONDS = 15;
488
487
  /** Enables the blocking review-evidence policy. */
489
488
  const REQUIRE_REVIEW_EVIDENCE_FLAG = "--require-review-evidence";
490
489
 
490
+ /**
491
+ * The flag that used to select the live-ruleset drift mode, retired with it.
492
+ *
493
+ * Kept as an explicit rejection rather than left to the argument parser. Every
494
+ * unrecognised `--*` argument is discarded before the positional read, so a
495
+ * caller that still selects the retired mode would otherwise get the ordinary
496
+ * offline run and an exit code of zero — a caller asking for a live comparison
497
+ * and being told "pass" by something that never looked. Retiring a mode
498
+ * silently is how a removed control keeps reporting success.
499
+ */
500
+ const RETIRED_REMOTE_FLAG = "--remote";
501
+
502
+ /**
503
+ * What a caller that still selects the retired mode is told.
504
+ *
505
+ * Names the flag only to say it is gone: the caller has to be able to find the
506
+ * thing it must delete. It is deliberately not a suggestion to run anything —
507
+ * there is no live-ruleset mode left to point at.
508
+ */
509
+ const RETIRED_REMOTE_MESSAGE = `check-skipped-required-checks: \`${RETIRED_REMOTE_FLAG}\` was retired together with the live-ruleset drift arm and the standing \`administration:read\` token it required (CodySwannGT/lisa#3599). There is no live comparison left to run, so this invocation would otherwise have reported success without performing the check that was asked for. Drop the flag, and drop the \`check:skipped-required-checks:remote\` npm script that passes it.`;
510
+
491
511
  /** Matches the `GITHUB_REF` a `pull_request` event run carries. */
492
512
  const REF_PULL = /^refs\/pull\/(\d+)\/(?:merge|head)$/u;
493
513
 
@@ -829,11 +849,19 @@ export function collectSkipJobTokens(rootDir, workflows) {
829
849
  * the state Lisa's seeds ship in, and the seeds are a GUESS — a guess that was
830
850
  * measured wrong (#2476). No stamp, no answer.
831
851
  *
852
+ * Trust is PRESENCE, not freshness (#3599). There is no age ceiling here and
853
+ * there must not be one: the scheduled arm that could re-read a live ruleset
854
+ * and re-stamp this file is gone, so an expiry would be a deadline nobody can
855
+ * meet, and the guard would go quietly inert rather than loudly wrong. A
856
+ * warning past N days is the same mistake wearing a softer hat — it recreates
857
+ * the obligation and emits a signal no operator can discharge. The protection
858
+ * that matters is the REFUSAL on an untranscribed snapshot, and that is what
859
+ * the two arms below preserve.
860
+ *
832
861
  * @param {object} declaration - The per-repo declaration
833
- * @param {number} [now] - Current epoch milliseconds, injectable for tests
834
862
  * @returns {{trusted: boolean, reason: string}} Whether to believe the snapshot, and why not
835
863
  */
836
- export function snapshotTrust(declaration, now = Date.now()) {
864
+ export function snapshotTrust(declaration) {
837
865
  const stamp = declaration.ruleset?.baseline_fetched_at;
838
866
  if (typeof stamp !== "string" || stamp.trim() === "") {
839
867
  return {
@@ -841,20 +869,12 @@ export function snapshotTrust(declaration, now = Date.now()) {
841
869
  reason: `\`ruleset.baseline_fetched_at\` is empty, so \`required_contexts\` has never been transcribed from a live ruleset. Lisa's seed ships a GUESS, and the guess was measured WRONG once in this fleet (#2476): in that repository it named a context no ruleset required and omitted six that were. It names no context here on purpose — what is required is a per-repository fact. Transcribe the real list, stamp the date, and this guard starts answering.`,
842
870
  };
843
871
  }
844
- const fetchedAt = Date.parse(stamp);
845
- if (Number.isNaN(fetchedAt)) {
872
+ if (Number.isNaN(Date.parse(stamp))) {
846
873
  return {
847
874
  trusted: false,
848
875
  reason: `\`ruleset.baseline_fetched_at\` is ${JSON.stringify(stamp)}, which is not a date this can read. Use an ISO-8601 timestamp, e.g. "2026-08-13".`,
849
876
  };
850
877
  }
851
- const ageDays = (now - fetchedAt) / 86_400_000;
852
- if (ageDays > SNAPSHOT_MAX_AGE_DAYS) {
853
- return {
854
- trusted: false,
855
- reason: `\`required_contexts\` was last transcribed ${Math.floor(ageDays)} days ago, past the ${SNAPSHOT_MAX_AGE_DAYS}-day ceiling. Rulesets are edited in an admin console with no signal in this repository, so a stale transcription is a memory of evidence rather than evidence.`,
856
- };
857
- }
858
878
  return { trusted: true, reason: "" };
859
879
  }
860
880
 
@@ -1897,67 +1917,6 @@ export function vacuityRefusal(input) {
1897
1917
  return null;
1898
1918
  }
1899
1919
 
1900
- /**
1901
- * Fetches the live required contexts for every declared ruleset.
1902
- *
1903
- * @param {object} ruleset - `{ repo, ids }` from the declaration
1904
- * @returns {string[]} Live contexts across all declared rulesets
1905
- * @throws {Error} When `gh` is unavailable or the API cannot be read
1906
- */
1907
- export function fetchLiveRequiredContexts(ruleset) {
1908
- if (
1909
- !ruleset?.repo ||
1910
- !Array.isArray(ruleset.ids) ||
1911
- ruleset.ids.length === 0
1912
- ) {
1913
- throw new Error(
1914
- `check-skipped-required-checks: --remote needs \`ruleset.repo\` and \`ruleset.ids\` in ${DECLARATION_PATH}.`
1915
- );
1916
- }
1917
- const contexts = [];
1918
- for (const id of ruleset.ids) {
1919
- const raw = boundedExecFileSync(
1920
- "gh",
1921
- [
1922
- "api",
1923
- `repos/${ruleset.repo}/rulesets/${id}`,
1924
- "--jq",
1925
- '.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context',
1926
- ],
1927
- { encoding: "utf8" }
1928
- );
1929
- for (const line of raw.split("\n").map(value => value.trim())) {
1930
- if (line !== "" && !contexts.includes(line)) contexts.push(line);
1931
- }
1932
- }
1933
- return contexts;
1934
- }
1935
-
1936
- /**
1937
- * Diffs the committed snapshot against the live ruleset, in BOTH directions.
1938
- *
1939
- * Both directions matter. A context added in the admin console makes the
1940
- * snapshot UNDER-detect (acmeorgd's ten-to-eleven drift, unnoticed for a day with
1941
- * every test green). A context removed there makes it OVER-detect, and the
1942
- * obvious fix for a false alarm is to weaken the guard.
1943
- *
1944
- * @param {ReadonlyArray<string>} snapshot - Committed contexts
1945
- * @param {ReadonlyArray<string>} live - Contexts from the API
1946
- * @returns {object[]} Drift violations
1947
- */
1948
- export function compareRulesetBaseline(snapshot, live) {
1949
- const added = live.filter(context => !snapshot.includes(context));
1950
- const removed = snapshot.filter(context => !live.includes(context));
1951
- if (added.length === 0 && removed.length === 0) return [];
1952
- return [
1953
- {
1954
- kind: VIOLATIONS.remoteDrift,
1955
- token: null,
1956
- message: `\`required_contexts\` has drifted from the live ruleset.${added.length ? `\n Live but not committed (the snapshot UNDER-detects): ${added.map(name => `"${name}"`).join(", ")}` : ""}${removed.length ? `\n Committed but not live (the snapshot OVER-detects): ${removed.map(name => `"${name}"`).join(", ")}` : ""}\n Update the snapshot and re-read what it now implies about the skip declarations.`,
1957
- },
1958
- ];
1959
- }
1960
-
1961
1920
  /**
1962
1921
  * Reads a `--name=<seconds>` flag, falling back when absent or unreadable.
1963
1922
  *
@@ -2071,17 +2030,18 @@ export function inspectVacuity(argv, declaration, options = {}) {
2071
2030
  /**
2072
2031
  * Runs the guard.
2073
2032
  *
2074
- * `--remote` reads the ruleset live, so it does not need the cache to be
2075
- * trustworthy — it is the thing that MAKES it trustworthy. Under `--remote` the
2076
- * required-context rules therefore run regardless of the stamp, and any
2077
- * disagreement surfaces as `ruleset_snapshot_drift` rather than as a verdict
2078
- * about skips.
2033
+ * Every arm is OFFLINE. There is no network read and no token anywhere in this
2034
+ * path: the required-context rules answer from the committed snapshot when
2035
+ * {@link snapshotTrust} believes it, and refuse when it does not.
2079
2036
  *
2080
2037
  * @param {ReadonlyArray<string>} argv - CLI arguments
2081
2038
  * @param {object} [options] - Injection seams forwarded to {@link inspectVacuity}
2082
2039
  * @returns {{violations: object[], checked: number, tokens: string[], enforcement: string, trust: {trusted: boolean, reason: string}, recipe: string, pr: string|undefined, evidenceChecked: number, vacuity: object|undefined}} The result
2083
2040
  */
2084
2041
  export function runGuard(argv, options = {}) {
2042
+ if (argv.includes(RETIRED_REMOTE_FLAG)) {
2043
+ throw new Error(RETIRED_REMOTE_MESSAGE);
2044
+ }
2085
2045
  if (
2086
2046
  argv.includes(REQUIRE_REVIEW_EVIDENCE_FLAG) &&
2087
2047
  !argv.includes("--vacuity") &&
@@ -2095,26 +2055,11 @@ export function runGuard(argv, options = {}) {
2095
2055
  const rootDir = positional[0] ?? process.cwd();
2096
2056
  const declaration = loadDeclaration(rootDir);
2097
2057
  const collected = collectSkipJobTokens(rootDir, declaration.workflows);
2098
- const remote = argv.includes("--remote");
2099
- const live = remote
2100
- ? fetchLiveRequiredContexts(declaration.ruleset)
2101
- : undefined;
2102
- const trust = remote
2103
- ? { trusted: true, reason: "" }
2104
- : snapshotTrust(declaration);
2105
- const result = evaluateSkippedRequiredChecks(
2106
- live === undefined
2107
- ? declaration
2108
- : { ...declaration, required_contexts: live },
2109
- collected.tokens,
2110
- { trustRequiredContexts: trust.trusted }
2111
- );
2058
+ const trust = snapshotTrust(declaration);
2059
+ const result = evaluateSkippedRequiredChecks(declaration, collected.tokens, {
2060
+ trustRequiredContexts: trust.trusted,
2061
+ });
2112
2062
  const violations = [...collected.violations, ...result.violations];
2113
- if (live !== undefined) {
2114
- violations.push(
2115
- ...compareRulesetBaseline(declaration.required_contexts, live)
2116
- );
2117
- }
2118
2063
 
2119
2064
  // The vacuity arm is layered ON TOP of the offline run rather than replacing
2120
2065
  // it: it is a third variant of one family, so it belongs in one report. Its
@@ -2237,8 +2182,6 @@ function main(argv) {
2237
2182
  "```",
2238
2183
  result.recipe,
2239
2184
  "```",
2240
- "",
2241
- `Meanwhile \`--remote\` answers WITHOUT the cache, because it reads the ruleset live: \`npm run check:skipped-required-checks:remote\`.`,
2242
2185
  ""
2243
2186
  );
2244
2187
  process.stderr.write(
@@ -7,7 +7,7 @@
7
7
  "To arm it: gh api repos/OWNER/NAME/rulesets --jq '.[] | \"\\(.id) \\(.name)\"' to find the ids, then gh api repos/OWNER/NAME/rulesets/ID --jq '.rules[] | select(.type==\"required_status_checks\") | .parameters.required_status_checks[].context' for the contexts. Paste them into `required_contexts` BYTE FOR BYTE (emoji and the ' / ' separator included), fill in `ruleset.repo` / `ruleset.ids`, and set `baseline_fetched_at` to today.",
8
8
  "The guard compares with exact string equality on purpose: a repo routinely carries confusable pairs (an external app's required `GitGuardian Security Checks` beside a skippable, not-required `🔐 Credential Leakage` proving the same property; `🧹 Lint` beside `🐢 Slow Lint Rules`, whose skip tokens `lint` and `lint_slow` are a strict prefix pair), and a fuzzy match would raise a false alarm whose obvious fix is to delete the guard.",
9
9
  "The worst confusable pair Lisa ever shipped was a NOT-required `🧪 Run Tests` beside the required `🧪 Run Unit Tests` — it merged red on two PRs because 'Run Tests failed' reads like the required test gate failing. It was DELETED in #2485, not renamed, because it only re-ran what the two coverage-carrying required contexts already run. If you find a near-duplicate advisory check in your own roster, prefer deleting it over trusting a reader to tell three similar names apart.",
10
- "A transcription expires after 90 days, because a ruleset can be edited with no signal in this repository. The shipped `.github/workflows/required-checks-drift.yml` runs `--remote` weekly to catch that; `--remote` reads the ruleset live and so answers even when the cache is untrusted.",
10
+ "A transcription NEVER EXPIRES. Trust is presence-based: a non-empty, parseable `baseline_fetched_at` means somebody read the list off a live ruleset, and it is believed; an empty or unreadable one means it was never transcribed, and the guard refuses to answer. A ninety-day ceiling used to sit here alongside a scheduled workflow that re-read the live ruleset and re-stamped this file. Both were removed (#3599) because that workflow needed a standing `administration:read` token in every repository. A deadline with no refresh mechanism is an obligation nobody can discharge, and a guard that reliably expires into NOT CHECKED looks healthy right up until it has been quietly inert for a quarter. The consequence is stated plainly: if this list and your live ruleset drift apart, you find out by consequence rather than by check. Re-transcribe when you change your ruleset.",
11
11
  "Lisa's quality.yml runs the offline arm on every pull request. This seed ships `\"enforcement\": \"warn\"`, which downgrades findings AND the NOT-CHECKED refusal to reports so a fresh install does not go red on arrival. Delete the key once you have transcribed the list — then it blocks.",
12
12
  "`skip_jobs` is matched as an exact comma-delimited token and GitHub Actions expression syntax has no string-replace, so write the list with no spaces: `skip_jobs: 'a,b'`, never `skip_jobs: 'a, b'`. A spaced token matches nothing and the job runs — reported as `whitespace_in_skip_token`.",
13
13
  "`_example_required_contexts` below is a STARTING POINT FOR TYPING, never read by the guard. Verify every line against your own ruleset before promoting any of it.",
@@ -11,7 +11,7 @@
11
11
  #
12
12
  # WHY THIS WORKFLOW EXISTS. The rule that detects it shipped long before
13
13
  # anything invoked it: `quality.yml` runs the guard's OFFLINE arm with no `--pr`,
14
- # `required-checks-drift.yml` runs `--remote` on a schedule, and the package
14
+ # a scheduled drift workflow ran its own arm, and the package
15
15
  # script named for the vacuous check was a bare invocation. So the detection was
16
16
  # `declared-but-uncallable` — the very family the guard exists to describe,
17
17
  # one level up inside its own shipping. Measured on the repository that owns it:
@@ -49,7 +49,8 @@
49
49
  ".github/workflows/reusable-claude-nightly-code-complexity-rails.yml",
50
50
  ".github/workflows/reusable-claude-nightly-jira-triage.yml",
51
51
  ".github/workflows/reusable-claude-nightly-test-coverage-rails.yml",
52
- ".github/workflows/reusable-claude-nightly-test-improvement-rails.yml"
52
+ ".github/workflows/reusable-claude-nightly-test-improvement-rails.yml",
53
+ ".github/workflows/required-checks-drift.yml"
53
54
  ],
54
55
  "keep": []
55
56
  }
@@ -22,7 +22,6 @@
22
22
  "prepare": "node -e \"if (process.env.INIT_CWD?.includes('.serverless')) process.exit(0); process.exit(1);\" || husky install || true",
23
23
  "nightly:health": "node scripts/check-nightly-e2e-health.mjs",
24
24
  "check:skipped-required-checks": "node scripts/check-skipped-required-checks.mjs",
25
- "check:skipped-required-checks:remote": "node scripts/check-skipped-required-checks.mjs --remote",
26
25
  "check:vacuous-required-checks": "node scripts/check-skipped-required-checks.mjs --vacuity",
27
26
  "test:node": "LISA_TEST_SCRATCH_PREFIXES='[\"node-\"]' lisa-test-run --profile typescript --adapter direct -- node scripts/lisa-test-node.mjs",
28
27
  "check:work-item": "node scripts/lisa-work-item.mjs validate-pr",
@@ -110,7 +109,8 @@
110
109
  ],
111
110
  "scripts": [
112
111
  "knip",
113
- "knip:lisa"
112
+ "knip:lisa",
113
+ "check:skipped-required-checks:remote"
114
114
  ]
115
115
  },
116
116
  "adopt": {
@@ -1,91 +0,0 @@
1
- # Seeded by Lisa on first setup — this file is YOURS.
2
- # Lisa will not overwrite it. (copy-overwrite assets ARE replaced each run.)
3
- # -----------------------------------------------------------------------------
4
- # Required-checks snapshot drift — the REMOTE arm (#2426)
5
- # -----------------------------------------------------------------------------
6
- # `.github/required-checks.json` holds two reviewed snapshots that police each
7
- # other: the contexts your ruleset requires, and what each `skip_jobs` token
8
- # silences. Two snapshots inside one repository can only catch each other
9
- # drifting from the CODE. Neither can see the RULESET change, because rulesets
10
- # are edited in an admin console — which is exactly how one repo's list went
11
- # from ten required contexts to eleven with every test still green for a day.
12
- #
13
- # So this runs the `--remote` arm on a schedule, and the pull-request path stays
14
- # offline. That split is the whole design: an enforced gate that needed network
15
- # and `gh` auth on every run would flake, and a flaky gate gets skipped — which
16
- # reintroduces the false-green class the guard exists to refuse. This workflow
17
- # never runs on `pull_request`, so it cannot wedge a merge.
18
- #
19
- # `--remote` is also the arm that STRUCTURALLY CANNOT LIE (#2476). It evaluates
20
- # the skip rules against the contexts it just read from the live ruleset, not
21
- # against the committed cache, so it answers correctly even in a repository
22
- # whose `required_contexts` has never been transcribed — which is exactly the
23
- # state the offline arm refuses to answer in. Arming this workflow is therefore
24
- # the fastest way to get a real answer.
25
- #
26
- # Reading a repository ruleset needs a token with administration:read. The
27
- # default GITHUB_TOKEN generally does not have it, so provide a PAT as the
28
- # repository secret RULESET_READ_TOKEN. Without it this job goes red rather
29
- # than reporting a clean bill of health it never checked.
30
- #
31
- # Until `ruleset.repo` / `ruleset.ids` are filled in, this reports UNCONFIGURED
32
- # and passes. Find the ids with:
33
- # gh api repos/OWNER/NAME/rulesets --jq '.[] | "\(.id) \(.name)"'
34
-
35
- name: 🔒 Required-Checks Drift
36
-
37
- on:
38
- schedule:
39
- # Weekly. A ruleset can be edited in the admin console on a day nobody
40
- # touched the repository, so change-triggered checking alone would miss it.
41
- - cron: '0 8 * * 1'
42
- workflow_dispatch:
43
-
44
- permissions:
45
- contents: read
46
-
47
- jobs:
48
- drift:
49
- name: 🔒 Ruleset snapshot vs live ruleset
50
- runs-on: ubuntu-latest
51
- timeout-minutes: 10
52
-
53
- steps:
54
- - name: 📥 Checkout repository
55
- uses: actions/checkout@v6
56
- with:
57
- persist-credentials: false
58
-
59
- - name: 🔧 Setup Node.js
60
- uses: actions/setup-node@v6
61
- with:
62
- package-manager-cache: false
63
- node-version: '22.21.1'
64
-
65
- - name: 🔒 Compare the committed snapshot against the live ruleset
66
- env:
67
- GH_TOKEN: ${{ secrets.RULESET_READ_TOKEN || github.token }}
68
- run: |
69
- set -euo pipefail
70
- # An absent input is a FAILURE, never a skip — the same flip #2933
71
- # made to the `🔒 Skipped Required Checks` job in quality.yml, for the
72
- # same reason: this workflow is installed by the same `lisa apply`
73
- # that installs the prover and the seed, so neither can be absent
74
- # without the install being broken, and a weekly run that reports
75
- # success having compared nothing is the drift it exists to catch.
76
- if [ ! -f scripts/check-skipped-required-checks.mjs ]; then
77
- echo "::error title=Skipped-required-check prover missing::scripts/check-skipped-required-checks.mjs does not exist, so the committed snapshot was compared against nothing. Run \`lisa apply\` to install the Lisa-managed script."
78
- exit 1
79
- fi
80
- if [ ! -f .github/required-checks.json ]; then
81
- echo "::error title=Required-checks declaration missing::.github/required-checks.json does not exist, so there is no committed snapshot to compare against the live ruleset. It is create-only and Lisa ships a seed — run \`lisa apply\` to get it."
82
- exit 1
83
- fi
84
- configured="$(node -e "const d=require('./.github/required-checks.json');process.stdout.write(String(Boolean(d.ruleset&&d.ruleset.repo&&Array.isArray(d.ruleset.ids)&&d.ruleset.ids.length)))")"
85
- if [ "$configured" != "true" ]; then
86
- echo "UNCONFIGURED: .github/required-checks.json has no \`ruleset.repo\` / \`ruleset.ids\`, so drift cannot be checked."
87
- echo "Fill them in to arm this: gh api repos/OWNER/NAME/rulesets --jq '.[] | \"\\(.id) \\(.name)\"'"
88
- echo "::warning title=Required-checks drift UNCHECKED::Fill in ruleset.repo and ruleset.ids in .github/required-checks.json to arm the remote drift check."
89
- exit 0
90
- fi
91
- node scripts/check-skipped-required-checks.mjs --remote