@codyswann/lisa 4.87.9 → 4.87.11

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 (88) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +2 -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/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.js +5 -3
  8. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  9. package/package.json +4 -4
  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-github-evidence/SKILL.md +57 -6
  13. package/plugins/lisa/skills/lisa-github-evidence/SKILL.md +57 -6
  14. package/plugins/lisa-agy/plugin.json +1 -1
  15. package/plugins/lisa-agy/skills/lisa-github-evidence/SKILL.md +57 -6
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/skills/lisa-github-evidence/SKILL.md +57 -6
  25. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cursor/skills/lisa-github-evidence/SKILL.md +57 -6
  27. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  29. package/plugins/lisa-devin/skills/lisa-github-evidence/SKILL.md +57 -6
  30. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-agy/plugin.json +1 -1
  33. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  54. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  58. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  61. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails-agy/plugin.json +1 -1
  68. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  74. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  75. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  79. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  81. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  82. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  86. package/plugins/src/base/skills/lisa-github-evidence/SKILL.md +57 -6
  87. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +146 -38
  88. package/typescript/create-only/.github/required-checks.json +1 -1
@@ -1848,12 +1848,12 @@ function normaliseBaseRef(ref) {
1848
1848
  * Resolves the branch the pull request under inspection merges INTO.
1849
1849
  *
1850
1850
  * Most-explicit-first, the same order {@link resolvePullRequestNumber} uses:
1851
- * an operator's `--base`, then `GITHUB_BASE_REF` (set by Actions on every
1852
- * `pull_request` and `pull_request_target` run), then the event payload's
1853
- * `pull_request.base.ref`. Offline on purpose — every source is a fact this run
1854
- * already holds, so selecting a set costs no API call, no token scope and no
1855
- * rate limit, and works the same on a fork pull request. Returning null is a
1856
- * real answer: the caller falls back to the union and SAYS so.
1851
+ * an operator's `--base`, then current metadata for the inspected PR, then
1852
+ * `GITHUB_BASE_REF` and the event payload's `pull_request.base.ref`. A rerun
1853
+ * replays its old event even after retargeting. The caller supplies metadata
1854
+ * from its existing head observation, so this pure selection adds no API call.
1855
+ * When current metadata is unavailable, the identity-checked event remains a
1856
+ * fallback. Returning null selects the reported union fallback.
1857
1857
  *
1858
1858
  * The run's own base describes the run's own pull request, and nothing else.
1859
1859
  * When `options.pr` names the pull request being inspected, GITHUB_BASE_REF
@@ -1867,13 +1867,21 @@ function normaliseBaseRef(ref) {
1867
1867
  *
1868
1868
  * @param {ReadonlyArray<string>} argv - CLI arguments
1869
1869
  * @param {NodeJS.ProcessEnv} [env] - Environment, injectable for tests
1870
- * @param {{pr?: string, repo?: string}} [options] - The pull request and repository being inspected, when known
1870
+ * @param {{pr?: string, repo?: string, currentPullRequest?: object}} [options] - Inspected identity and its current metadata, when available
1871
1871
  * @returns {{base: string|null, source: string|null, ignoredEventBase?: string}} The base and where it came from
1872
1872
  */
1873
1873
  export function resolveBaseRef(argv, env = process.env, options = {}) {
1874
1874
  const explicit = normaliseBaseRef(readFlagValue(argv, "--base"));
1875
1875
  if (explicit !== null) return { base: explicit, source: "--base" };
1876
1876
 
1877
+ if (
1878
+ isInspectedPullRequest(options.currentPullRequest, options.pr, options.repo)
1879
+ ) {
1880
+ const current = normaliseBaseRef(options.currentPullRequest.baseRefName);
1881
+ if (current !== null)
1882
+ return { base: current, source: "current PR metadata" };
1883
+ }
1884
+
1877
1885
  const eventPath = env.GITHUB_EVENT_PATH;
1878
1886
  const payload =
1879
1887
  eventPath !== undefined && eventPath !== "" && existsSync(eventPath)
@@ -1897,6 +1905,34 @@ export function resolveBaseRef(argv, env = process.env, options = {}) {
1897
1905
  : { base: null, source: null, ignoredEventBase: found.base };
1898
1906
  }
1899
1907
 
1908
+ /**
1909
+ * Match both native metadata identities before trusting its current base.
1910
+ * @param {object|undefined} current - Native PR metadata
1911
+ * @param {string|number|undefined} pr - Inspected PR
1912
+ * @param {string|undefined} repo - Inspected repository
1913
+ * @returns {boolean} Whether this is the requested PR in the requested repo
1914
+ */
1915
+ function isInspectedPullRequest(current, pr, repo) {
1916
+ if (
1917
+ !current ||
1918
+ pr === undefined ||
1919
+ repo === undefined ||
1920
+ String(current.number) !== String(pr) ||
1921
+ typeof current.url !== "string"
1922
+ )
1923
+ return false;
1924
+ try {
1925
+ const url = new URL(current.url);
1926
+ return (
1927
+ ["https:", "http:"].includes(url.protocol) &&
1928
+ url.pathname.replace(/\/$/u, "").toLowerCase() ===
1929
+ `/${repo}/pull/${pr}`.toLowerCase()
1930
+ );
1931
+ } catch {
1932
+ return false;
1933
+ }
1934
+ }
1935
+
1900
1936
  /**
1901
1937
  * True when the inspected repository is the one this run's event belongs to.
1902
1938
  *
@@ -4051,6 +4087,45 @@ export function resolveHeadSha(pr, repo) {
4051
4087
  }
4052
4088
  }
4053
4089
 
4090
+ /**
4091
+ * Observe head and current base together using the existing PR metadata read.
4092
+ * A failed metadata read retains the old head probe, so an independently
4093
+ * readable head can still use the declared event/union base fallbacks.
4094
+ * @param {string|number} pr - Inspected PR
4095
+ * @param {string|undefined} repo - Inspected repository
4096
+ * @returns {{headSha: string|undefined, repo?: string, pullRequest?: object}} One observation
4097
+ */
4098
+ function observePullRequest(pr, repo) {
4099
+ const slug = resolveRepoSlug(repo);
4100
+ if (slug === undefined) return { headSha: undefined };
4101
+ try {
4102
+ const current = JSON.parse(
4103
+ boundedExecFileSync(
4104
+ "gh",
4105
+ [
4106
+ "pr",
4107
+ "view",
4108
+ String(pr),
4109
+ "--repo",
4110
+ slug,
4111
+ "--json",
4112
+ "number,url,headRefOid,baseRefName",
4113
+ ],
4114
+ { encoding: "utf8" }
4115
+ )
4116
+ );
4117
+ if (
4118
+ isInspectedPullRequest(current, pr, slug) &&
4119
+ typeof current.headRefOid === "string" &&
4120
+ /^[a-f0-9]{40}$/iu.test(current.headRefOid)
4121
+ )
4122
+ return { headSha: current.headRefOid, repo: slug, pullRequest: current };
4123
+ } catch {
4124
+ // Current metadata is unavailable; preserve the existing head-only probe.
4125
+ }
4126
+ return { headSha: resolveHeadSha(pr, slug), repo: slug };
4127
+ }
4128
+
4054
4129
  /**
4055
4130
  * How a finding cites the commit its evidence was read at.
4056
4131
  *
@@ -4386,7 +4461,7 @@ function sleepSync(ms) {
4386
4461
  * @param {object} declaration - The per-repo declaration
4387
4462
  * @param {string|number} pr - Pull request number
4388
4463
  * @param {string|undefined} repo - `OWNER/NAME`, or undefined
4389
- * @param {{timeoutSeconds?: number, intervalSeconds?: number, fetch?: Function, now?: Function, sleep?: Function, headSha?: Function, requiredSet?: object, substitutes?: Set<string>, substituteHead?: string}} [options] -
4464
+ * @param {{timeoutSeconds?: number, intervalSeconds?: number, fetch?: Function, now?: Function, sleep?: Function, headSha?: Function, observe?: Function, initialObservation?: object, requiredSet?: object, requiredSetFor?: Function, substitutes?: Set<string>, substituteHead?: string}} [options] -
4390
4465
  * Injection seams; the suite drives the whole loop through them so nothing
4391
4466
  * here has to sleep in real time
4392
4467
  * @returns {{checks: object[], settled: boolean, headSha: string|undefined, registration: object}} Rows, reviewer completion, head and required-registration observations
@@ -4411,16 +4486,20 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
4411
4486
  );
4412
4487
  });
4413
4488
  const resolveHead = options.headSha ?? resolveHeadSha;
4489
+ const observe =
4490
+ options.observe ??
4491
+ ((request, slug) => ({ headSha: resolveHead(request, slug) }));
4414
4492
  const clock = options.now ?? Date.now;
4415
4493
  const sleep = options.sleep ?? sleepSync;
4416
4494
  const started = clock();
4417
4495
  const deadline = started + timeoutMs;
4418
- const requiredSet =
4496
+ const defaultRequiredSet =
4419
4497
  options.requiredSet ??
4420
4498
  selectRequiredContexts(declaration, { base: null, source: null });
4421
- const missing = checks =>
4422
- requiredSet.contexts.filter(
4423
- name => !checks.some(check => check.name === name)
4499
+ const selectSet = options.requiredSetFor ?? (() => defaultRequiredSet);
4500
+ const missing = observed =>
4501
+ observed.requiredSet.contexts.filter(
4502
+ name => !observed.checks.some(check => check.name === name)
4424
4503
  );
4425
4504
  // A substitute is useful only at the head it was actually read for. Pending
4426
4505
  // rows still wait, and a missing required reviewer still needs a real row.
@@ -4435,29 +4514,39 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
4435
4514
  );
4436
4515
  });
4437
4516
 
4438
- // Bracket every roster read with head reads. `gh pr checks` targets the PR's
4439
- // current head but does not return the SHA it used, and the permission-light
4440
- // fallback resolves the head internally. A push between either route and a
4441
- // later provenance lookup used to make findings cite a commit whose checks
4442
- // were never evaluated. Matching observations on both sides bind the roster
4443
- // to that head; a mismatch is discarded and re-read, never reported.
4517
+ // Bracket checks with coherent head/base observations. The early substitute
4518
+ // read can supply the first observation without another metadata request.
4519
+ // A push or retarget during the read is discarded within this same deadline.
4520
+ let initial = options.initialObservation;
4444
4521
  const snapshot = () => {
4445
- const before = resolveHead(pr, repo);
4446
- if (before === undefined) {
4522
+ const before = initial?.headSha === undefined ? observe(pr, repo) : initial;
4523
+ initial = undefined;
4524
+ const beforeSet = selectSet(before);
4525
+ if (before.headSha === undefined) {
4447
4526
  return {
4448
4527
  checks: [],
4449
4528
  headSha: undefined,
4450
4529
  stable: false,
4451
4530
  after: undefined,
4531
+ requiredSet: beforeSet,
4532
+ baseBefore: beforeSet.base,
4533
+ baseAfter: undefined,
4452
4534
  };
4453
4535
  }
4454
- const checks = read(pr, repo, before);
4455
- const after = resolveHead(pr, repo);
4536
+ const checks = read(pr, repo, before.headSha);
4537
+ const after = observe(pr, repo);
4538
+ const afterSet = selectSet(after);
4456
4539
  return {
4457
4540
  checks,
4458
- headSha: before,
4459
- stable: after !== undefined && before === after,
4460
- after,
4541
+ headSha: before.headSha,
4542
+ stable:
4543
+ after.headSha !== undefined &&
4544
+ before.headSha === after.headSha &&
4545
+ beforeSet.base === afterSet.base,
4546
+ after: after.headSha,
4547
+ requiredSet: afterSet,
4548
+ baseBefore: beforeSet.base,
4549
+ baseAfter: afterSet.base,
4461
4550
  };
4462
4551
  };
4463
4552
 
@@ -4465,20 +4554,24 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
4465
4554
  while (
4466
4555
  (!observed.stable ||
4467
4556
  !evidenceReady(observed) ||
4468
- missing(observed.checks).length > 0) &&
4557
+ missing(observed).length > 0) &&
4469
4558
  clock() < deadline
4470
4559
  ) {
4471
4560
  sleep(Math.min(intervalMs, deadline - clock()));
4472
4561
  observed = snapshot();
4473
4562
  }
4474
4563
  if (!observed.stable) {
4564
+ if (observed.headSha !== undefined && observed.headSha === observed.after)
4565
+ throw new Error(
4566
+ `check-skipped-required-checks: the selected pull request base changed while its checks were being read (${observed.baseBefore ?? "unresolved"} -> ${observed.baseAfter ?? "unresolved"}). Refusing to judge checks against another base; re-read the pull request after retargeting settles.`
4567
+ );
4475
4568
  throw new Error(
4476
4569
  `check-skipped-required-checks: the pull request head changed while its checks were being read (${observed.headSha ?? "unresolved"} -> ${observed.after ?? "unresolved"}). Refusing to attach one commit's review evidence to another; re-read the pull request after the push settles.`
4477
4570
  );
4478
4571
  }
4479
4572
  const elapsedSeconds = Math.max(0, clock() - started) / 1000;
4480
- const unregistered = missing(observed.checks);
4481
- const total = requiredSet.contexts.length;
4573
+ const unregistered = missing(observed);
4574
+ const total = observed.requiredSet.contexts.length;
4482
4575
  const present = total - unregistered.length;
4483
4576
  const expired = unregistered.length > 0;
4484
4577
  const settled = checksSettled(declaration, observed.checks);
@@ -4506,6 +4599,7 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
4506
4599
  headSha: observed.headSha,
4507
4600
  registration,
4508
4601
  evidence,
4602
+ requiredSet: observed.requiredSet,
4509
4603
  };
4510
4604
  }
4511
4605
 
@@ -4701,7 +4795,7 @@ function readSecondsFlag(argv, name, fallback) {
4701
4795
  *
4702
4796
  * @param {ReadonlyArray<string>} argv - CLI arguments
4703
4797
  * @param {object} declaration - The per-repo declaration
4704
- * @param {{trustRequiredContexts?: boolean, env?: NodeJS.ProcessEnv, fetch?: Function, probeBranch?: Function, now?: Function, sleep?: Function, headSha?: Function, reviewActivity?: Function}} [options] -
4798
+ * @param {{trustRequiredContexts?: boolean, env?: NodeJS.ProcessEnv, fetch?: Function, probeBranch?: Function, now?: Function, sleep?: Function, headSha?: Function, observe?: Function, reviewActivity?: Function}} [options] -
4705
4799
  * Injection seams for the suite
4706
4800
  * @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
4707
4801
  */
@@ -4755,19 +4849,30 @@ export function inspectVacuity(argv, declaration, options = {}) {
4755
4849
  const declaredReviewers = Object.keys(
4756
4850
  declaration.evidence_bearing_checks ?? {}
4757
4851
  );
4758
- const earlyHead = requireReviewEvidence
4759
- ? (options.headSha ?? resolveHeadSha)(pr, repo)
4852
+ const injectedHead = options.headSha;
4853
+ const observe =
4854
+ options.observe ??
4855
+ (injectedHead
4856
+ ? (request, slug) => ({ headSha: injectedHead(request, slug) })
4857
+ : observePullRequest);
4858
+ const earlyObservation = requireReviewEvidence
4859
+ ? observe(pr, repo)
4760
4860
  : undefined;
4861
+ const earlyHead = earlyObservation?.headSha;
4761
4862
  const earlySubstitutes = readSubstitutes(earlyHead);
4762
4863
  const allStoodIn =
4763
4864
  declaredReviewers.length > 0 &&
4764
4865
  declaredReviewers.every(name => earlySubstitutes.has(name));
4765
- // Select once: polling and the absence verdict must judge the same base.
4766
- // resolveBaseRef retains its existing contract; live retargeting is #4487.
4767
- const requiredSet = selectRequiredContexts(
4768
- declaration,
4769
- resolveBaseRef(argv, options.env ?? process.env, { pr, repo })
4770
- );
4866
+ // Registration and absence share the selection returned with stable checks.
4867
+ const requiredSetFor = observed =>
4868
+ selectRequiredContexts(
4869
+ declaration,
4870
+ resolveBaseRef(argv, options.env ?? process.env, {
4871
+ pr,
4872
+ repo: repo ?? observed.repo,
4873
+ currentPullRequest: observed.pullRequest,
4874
+ })
4875
+ );
4771
4876
  let read;
4772
4877
  try {
4773
4878
  read = fetchSettledChecks(declaration, pr, repo, {
@@ -4785,13 +4890,16 @@ export function inspectVacuity(argv, declaration, options = {}) {
4785
4890
  now: options.now,
4786
4891
  sleep: options.sleep,
4787
4892
  headSha: options.headSha,
4788
- requiredSet,
4893
+ observe,
4894
+ initialObservation: earlyObservation,
4895
+ requiredSetFor,
4789
4896
  substitutes: allStoodIn ? earlySubstitutes : undefined,
4790
4897
  substituteHead: earlyHead,
4791
4898
  });
4792
4899
  } catch (error) {
4793
4900
  return refused(vacuityRefusal({ declaration, pr, error }));
4794
4901
  }
4902
+ const requiredSet = read.requiredSet;
4795
4903
 
4796
4904
  const refusal = vacuityRefusal({ declaration, pr, checks: read.checks });
4797
4905
  if (refusal !== null) {
@@ -18,7 +18,7 @@
18
18
  "IT REFUSES RATHER THAN REPORTING AN EMPTY INSPECTION. An unresolvable pull request, a `gh` it could not read, a roster with no checks in it, or an empty `evidence_bearing_checks` all print the same \"nothing vacuous here\" that a genuinely clean run prints — so the guard fails instead and names which of the four it hit (`vacuity_pr_unresolved`, `vacuity_checks_unreadable`, `vacuity_no_checks_reported`, `vacuity_none_declared`). A refusal is NOT a finding: NOBODY LOOKED and THE REVIEW WAS FAKE are opposite facts, and a single red job would otherwise report them identically.",
19
19
  "`evidence_bearing_checks` names the checks whose GREEN is supposed to mean something reviewed the code. Use `{}` to accept the shipped description vocabulary, or add `proof` / `no_work` arrays to extend it — extensions ADD to the defaults, they do not replace them. Undeclared checks are never examined, because most CI jobs ship an empty description and flagging them all would bury the one finding that matters.",
20
20
  "This arm REPORTS AND NEVER BLOCKS, in every enforcement mode. A review bot can go hollow because an org-wide SPENDING CAP was hit, and a gate that reddens every PR on a billing state is worse than the one it criticises. What it changes is what you may CLAIM: a PR carrying a `vacuous_required_check` finding has not been shown to be reviewed, so do not record it as reviewed. Once you have made the governance call, `--fail-on-vacuous` is the supported way to ask for an exit code — add it to the command in `review-evidence.yml`, instead of writing a wrapper that reads `--json`.",
21
- "PER-BRANCH SETS (CodySwannGT/lisa#4484). `required_contexts` is the UNION across every protected branch, which is the wrong set for judging which required contexts did NOT REPORT on one pull request: a context only `dev` requires was reported absent on every staging-base pull request. Optionally add `required_contexts_by_branch: { \"<branch>\": [ ...contexts ] }`, each set transcribed byte for byte from `gh api repos/OWNER/NAME/rules/branches/<branch> --jq '.[] | select(.type==\"required_status_checks\") | .parameters.required_status_checks[].context'` (read access only). The absent arm then judges a pull request against its BASE branch's set (`--base`, else `GITHUB_BASE_REF`, else the event payload's `pull_request.base.ref`). Without a set for that base it falls back to the union — same findings as before — and says so as `required_contexts_branch_fallback`; every run prints which set it used. Every per-branch context must also appear in `required_contexts`, and the one `baseline_fetched_at` stamp covers both."
21
+ "PER-BRANCH SETS (CodySwannGT/lisa#4484). `required_contexts` is the UNION across every protected branch, which is the wrong set for judging which required contexts did NOT REPORT on one pull request: a context only `dev` requires was reported absent on every staging-base pull request. Optionally add `required_contexts_by_branch: { \"<branch>\": [ ...contexts ] }`, each set transcribed byte for byte from `gh api repos/OWNER/NAME/rules/branches/<branch> --jq '.[] | select(.type==\"required_status_checks\") | .parameters.required_status_checks[].context'` (read access only). The absent arm judges the PR against its current base (`--base`, else current metadata from the existing PR head read). If current metadata is unreadable it uses only a matching PR/repository event (`GITHUB_BASE_REF`, else `pull_request.base.ref`). A rerun replays the old event, so matching identity alone does not establish the current base. Head/base observations bracket each check read; a retarget retries within the same settlement deadline, and registration and absence share the resulting selection. Without a set for that base it falls back to the union — same findings as before — and says so as `required_contexts_branch_fallback`; every run prints which set it used. Every per-branch context must also appear in `required_contexts`, and the one `baseline_fetched_at` stamp covers both."
22
22
  ],
23
23
  "enforcement": "warn",
24
24
  "ruleset": {