@codyswann/lisa 4.84.4 → 4.84.5

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 (81) 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 +4 -3
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/package.json +4 -4
  9. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa-agy/plugin.json +1 -1
  12. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  15. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-devin/.devin-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-devin/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo-agy/plugin.json +1 -1
  26. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  54. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails-agy/plugin.json +1 -1
  61. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  68. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  75. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  79. package/scripts/two-channel-couplings.json +1 -1
  80. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +139 -48
  81. package/typescript/create-only/.github/workflows/review-evidence.yml +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.84.4",
3
+ "version": "4.84.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.84.4",
3
+ "version": "4.84.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.84.4",
3
+ "version": "4.84.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.84.4",
3
+ "version": "4.84.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.84.4",
3
+ "version": "4.84.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -103,7 +103,7 @@
103
103
  },
104
104
  "inspected": {
105
105
  "workflows": 26,
106
- "steps": 785,
106
+ "steps": 786,
107
107
  "couplings": 36,
108
108
  "inventory": 186
109
109
  },
@@ -1775,10 +1775,12 @@ export function evaluateAbsentRequiredChecks(
1775
1775
  return { violations: [], checked: 0, absent: [] };
1776
1776
  // #4484: judged against the BASE branch's own set when the declaration has
1777
1777
  // one, and against the union — reported, never silently — when it does not.
1778
- const requiredSet = selectRequiredContexts(
1779
- declaration,
1780
- options.base ?? { base: null, source: null }
1781
- );
1778
+ const requiredSet =
1779
+ options.requiredSet ??
1780
+ selectRequiredContexts(
1781
+ declaration,
1782
+ options.base ?? { base: null, source: null }
1783
+ );
1782
1784
  const required = requiredSet.contexts;
1783
1785
  const judgedFor =
1784
1786
  requiredSet.source === "branch"
@@ -1816,7 +1818,10 @@ export function evaluateAbsentRequiredChecks(
1816
1818
  kind: VIOLATIONS.absent,
1817
1819
  token: name,
1818
1820
  contexts: [name],
1819
- message: `\`${name}\` is ruleset-required but did not report on this pull request at all.${at}${judgedFor} It posted NO check-run — not pending, not failing, ABSENT. \`gh pr checks\` prints no line for it and the pull request page renders it exactly as one still in flight, so every surface says "wait" and none says "broken".${approval} (If the context was renamed, fix \`required_contexts\`; names are compared byte for byte.)`,
1821
+ message:
1822
+ options.registration === undefined
1823
+ ? `\`${name}\` is ruleset-required but did not report on this pull request at all.${at}${judgedFor} It posted NO check-run — not pending, not failing, ABSENT. \`gh pr checks\` prints no line for it and the pull request page renders it exactly as one still in flight, so every surface says "wait" and none says "broken".${approval} (If the context was renamed, fix \`required_contexts\`; names are compared byte for byte.)`
1824
+ : `${options.registration.line}${at}${judgedFor}${approval} The wait ending does not prove permanent absence or a defect in this change. If a required context was renamed or removed, reconcile its declared name with CI; names are compared byte for byte.`,
1820
1825
  })),
1821
1826
  };
1822
1827
  }
@@ -4152,12 +4157,23 @@ export function declaredEvidenceChecks(declaration) {
4152
4157
  * @returns {boolean} True when nothing declared is still in flight
4153
4158
  */
4154
4159
  export function checksSettled(declaration, checks) {
4155
- return declaredEvidenceChecks(declaration).every(name => {
4156
- const found = checks.find(check => check.name === name);
4157
- if (found === undefined) return false;
4158
- if (String(found.bucket ?? "").toLowerCase() === "pending") return false;
4159
- return String(found.state ?? "").toUpperCase() !== "PENDING";
4160
- });
4160
+ return declaredEvidenceChecks(declaration).every(name =>
4161
+ checkSettled(checks.find(check => check.name === name))
4162
+ );
4163
+ }
4164
+
4165
+ /**
4166
+ * Distinguishes a completed reviewer from an absent or in-flight row.
4167
+ *
4168
+ * @param {object|undefined} found - One normalized check row
4169
+ * @returns {boolean} Whether the row is terminal
4170
+ */
4171
+ function checkSettled(found) {
4172
+ return (
4173
+ found !== undefined &&
4174
+ String(found.bucket ?? "").toLowerCase() !== "pending" &&
4175
+ String(found.state ?? "").toUpperCase() !== "PENDING"
4176
+ );
4161
4177
  }
4162
4178
 
4163
4179
  /**
@@ -4176,15 +4192,20 @@ function sleepSync(ms) {
4176
4192
  }
4177
4193
 
4178
4194
  /**
4179
- * Reads a pull request's checks, re-reading until the declared ones settle.
4195
+ * Reads until reviewers settle and the selected required rows register.
4196
+ *
4197
+ * Registration needs a row, including a pending or failing row; reviewer
4198
+ * completion is a separate observation. Both waits share one absolute deadline.
4199
+ * A head-bound substitute can skip an unreported reviewer's evidence wait,
4200
+ * but never the registration of a ruleset-required context.
4180
4201
  *
4181
4202
  * @param {object} declaration - The per-repo declaration
4182
4203
  * @param {string|number} pr - Pull request number
4183
4204
  * @param {string|undefined} repo - `OWNER/NAME`, or undefined
4184
- * @param {{timeoutSeconds?: number, intervalSeconds?: number, fetch?: Function, now?: Function, sleep?: Function, headSha?: Function}} [options] -
4205
+ * @param {{timeoutSeconds?: number, intervalSeconds?: number, fetch?: Function, now?: Function, sleep?: Function, headSha?: Function, requiredSet?: object, substitutes?: Set<string>, substituteHead?: string}} [options] -
4185
4206
  * Injection seams; the suite drives the whole loop through them so nothing
4186
4207
  * here has to sleep in real time
4187
- * @returns {{checks: object[], settled: boolean, headSha: string|undefined}} The rows, whether they settled, and the head they were read at
4208
+ * @returns {{checks: object[], settled: boolean, headSha: string|undefined, registration: object}} Rows, reviewer completion, head and required-registration observations
4188
4209
  */
4189
4210
  export function fetchSettledChecks(declaration, pr, repo, options = {}) {
4190
4211
  const timeoutMs = (options.timeoutSeconds ?? SETTLE_TIMEOUT_SECONDS) * 1000;
@@ -4203,7 +4224,27 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
4203
4224
  const resolveHead = options.headSha ?? resolveHeadSha;
4204
4225
  const clock = options.now ?? Date.now;
4205
4226
  const sleep = options.sleep ?? sleepSync;
4206
- const deadline = clock() + timeoutMs;
4227
+ const started = clock();
4228
+ const deadline = started + timeoutMs;
4229
+ const requiredSet =
4230
+ options.requiredSet ??
4231
+ selectRequiredContexts(declaration, { base: null, source: null });
4232
+ const missing = checks =>
4233
+ requiredSet.contexts.filter(
4234
+ name => !checks.some(check => check.name === name)
4235
+ );
4236
+ // A substitute is useful only at the head it was actually read for. Pending
4237
+ // rows still wait, and a missing required reviewer still needs a real row.
4238
+ const evidenceReady = observed =>
4239
+ declaredEvidenceChecks(declaration).every(name => {
4240
+ const found = observed.checks.find(check => check.name === name);
4241
+ return (
4242
+ checkSettled(found) ||
4243
+ (found === undefined &&
4244
+ observed.headSha === options.substituteHead &&
4245
+ options.substitutes?.has(name))
4246
+ );
4247
+ });
4207
4248
 
4208
4249
  // Bracket every roster read with head reads. `gh pr checks` targets the PR's
4209
4250
  // current head but does not return the SHA it used, and the permission-light
@@ -4233,7 +4274,9 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
4233
4274
 
4234
4275
  let observed = snapshot();
4235
4276
  while (
4236
- (!observed.stable || !checksSettled(declaration, observed.checks)) &&
4277
+ (!observed.stable ||
4278
+ !evidenceReady(observed) ||
4279
+ missing(observed.checks).length > 0) &&
4237
4280
  clock() < deadline
4238
4281
  ) {
4239
4282
  sleep(Math.min(intervalMs, deadline - clock()));
@@ -4244,10 +4287,36 @@ export function fetchSettledChecks(declaration, pr, repo, options = {}) {
4244
4287
  `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.`
4245
4288
  );
4246
4289
  }
4290
+ const elapsedSeconds = Math.max(0, clock() - started) / 1000;
4291
+ const unregistered = missing(observed.checks);
4292
+ const total = requiredSet.contexts.length;
4293
+ const present = total - unregistered.length;
4294
+ const expired = unregistered.length > 0;
4295
+ const settled = checksSettled(declaration, observed.checks);
4296
+ const evidence = {
4297
+ settled,
4298
+ // A missing reviewer covered by a head-bound substitute did not finish,
4299
+ // but its evidence wait was skipped rather than expired. Reporting those
4300
+ // as the same fact contradicts the substituted verdict.
4301
+ expired: !evidenceReady(observed),
4302
+ line: `Review evidence ${settled ? "settled" : evidenceReady(observed) ? "covered by head-bound substitutes" : "wait EXPIRED"} after ${elapsedSeconds}s.`,
4303
+ };
4304
+ const registration = {
4305
+ elapsedSeconds,
4306
+ present,
4307
+ total,
4308
+ missing: unregistered,
4309
+ expired,
4310
+ line: expired
4311
+ ? `Required registration wait EXPIRED: the gate stopped waiting after ${elapsedSeconds}s; ${present} of ${total} required contexts observed; missing: ${unregistered.join(", ")}. RE-RUN this job before treating this as a finding about the change.`
4312
+ : `Required registration settled after ${elapsedSeconds}s; ${present} of ${total} required contexts observed.`,
4313
+ };
4247
4314
  return {
4248
4315
  checks: observed.checks,
4249
- settled: checksSettled(declaration, observed.checks),
4316
+ settled,
4250
4317
  headSha: observed.headSha,
4318
+ registration,
4319
+ evidence,
4251
4320
  };
4252
4321
  }
4253
4322
 
@@ -4504,14 +4573,19 @@ export function inspectVacuity(argv, declaration, options = {}) {
4504
4573
  const allStoodIn =
4505
4574
  declaredReviewers.length > 0 &&
4506
4575
  declaredReviewers.every(name => earlySubstitutes.has(name));
4507
- const settle = skipWait =>
4508
- fetchSettledChecks(declaration, pr, repo, {
4576
+ // Select once: polling and the absence verdict must judge the same base.
4577
+ // resolveBaseRef retains its existing contract; live retargeting is #4487.
4578
+ const requiredSet = selectRequiredContexts(
4579
+ declaration,
4580
+ resolveBaseRef(argv, options.env ?? process.env, { pr, repo })
4581
+ );
4582
+ let read;
4583
+ try {
4584
+ read = fetchSettledChecks(declaration, pr, repo, {
4509
4585
  timeoutSeconds: readSecondsFlag(
4510
4586
  argv,
4511
4587
  "--settle-timeout",
4512
- (wired || requireReviewEvidence) && !skipWait
4513
- ? SETTLE_TIMEOUT_SECONDS
4514
- : 0
4588
+ wired || requireReviewEvidence ? SETTLE_TIMEOUT_SECONDS : 0
4515
4589
  ),
4516
4590
  intervalSeconds: readSecondsFlag(
4517
4591
  argv,
@@ -4522,23 +4596,10 @@ export function inspectVacuity(argv, declaration, options = {}) {
4522
4596
  now: options.now,
4523
4597
  sleep: options.sleep,
4524
4598
  headSha: options.headSha,
4599
+ requiredSet,
4600
+ substitutes: allStoodIn ? earlySubstitutes : undefined,
4601
+ substituteHead: earlyHead,
4525
4602
  });
4526
- let read;
4527
- try {
4528
- read = settle(allStoodIn);
4529
- // The wait is skipped only for a reviewer that is NOT COMING. A declared
4530
- // reviewer whose row is already there but still in flight is coming, and
4531
- // a substitute does not stand in for it — so wait for its answer exactly
4532
- // as an ordinary run would, rather than going red on one snapshot.
4533
- if (
4534
- allStoodIn &&
4535
- !read.settled &&
4536
- declaredReviewers.some(name =>
4537
- read.checks.some(check => check.name === name)
4538
- )
4539
- ) {
4540
- read = settle(false);
4541
- }
4542
4603
  } catch (error) {
4543
4604
  return refused(vacuityRefusal({ declaration, pr, error }));
4544
4605
  }
@@ -4549,6 +4610,9 @@ export function inspectVacuity(argv, declaration, options = {}) {
4549
4610
  ...refused(refusal),
4550
4611
  headSha: read.headSha,
4551
4612
  settled: read.settled,
4613
+ requiredSet,
4614
+ registration: read.registration,
4615
+ evidence: read.evidence,
4552
4616
  };
4553
4617
  }
4554
4618
  const evaluated = evaluateVacuousChecks(declaration, read.checks, {
@@ -4562,9 +4626,8 @@ export function inspectVacuity(argv, declaration, options = {}) {
4562
4626
  const absent = evaluateAbsentRequiredChecks(declaration, read.checks, {
4563
4627
  headSha: read.headSha,
4564
4628
  parkedRuns: options.parkedRuns,
4565
- // Tied to the pull request actually inspected, so a `--pr` naming another
4566
- // pull request is never judged against this run's own base.
4567
- base: resolveBaseRef(argv, options.env ?? process.env, { pr, repo }),
4629
+ requiredSet,
4630
+ registration: read.registration,
4568
4631
  });
4569
4632
  // The gate is evaluated on EVERY run, not only when a caller asked it to
4570
4633
  // block. Its findings are what make a waiver visible, and a waiver that is
@@ -4572,10 +4635,9 @@ export function inspectVacuity(argv, declaration, options = {}) {
4572
4635
  // repositories that have not opted in yet.
4573
4636
  const gateOptions = {
4574
4637
  headSha: read.headSha,
4575
- // The settle loop exits on one of two things: everything declared reached a
4576
- // terminal read, or the deadline passed. So `settled === false` IS "the
4577
- // wait expired", and it is the only place that fact exists — every reader
4578
- // downstream sees a check row that looks identical either way (#3716).
4638
+ // Reviewer completion stays separate from required registration. Expiry
4639
+ // while an ordinary required row is missing must not turn a completed
4640
+ // reviewer into an undetermined one (#3716, #4527).
4579
4641
  waitExpired: read.settled === false,
4580
4642
  // Bound to the head the checks were read at. A push between the early read
4581
4643
  // and the settled one invalidates every substitute for the old head.
@@ -4674,7 +4736,9 @@ export function inspectVacuity(argv, declaration, options = {}) {
4674
4736
  ...(objection?.violations ?? []),
4675
4737
  ],
4676
4738
  absent: absent.absent,
4677
- requiredSet: absent.requiredSet,
4739
+ requiredSet,
4740
+ registration: read.registration,
4741
+ evidence: read.evidence,
4678
4742
  gateStates: gate.states,
4679
4743
  gateConditions: gate.conditions,
4680
4744
  gateDescriptions: gate.descriptions,
@@ -5392,7 +5456,7 @@ function appendCleanResult(lines, result, refusal) {
5392
5456
  );
5393
5457
  if (result.pr === undefined) return;
5394
5458
  const settleNote =
5395
- result.vacuity?.settled === false
5459
+ (result.vacuity?.evidence?.expired ?? result.vacuity?.settled === false)
5396
5460
  ? " (One or more had not settled when the wait expired, so this is what was true at that moment.)"
5397
5461
  : "";
5398
5462
  lines.push(
@@ -5444,7 +5508,7 @@ function appendFindingReport(lines, result, policy, blocking) {
5444
5508
  * @returns {void}
5445
5509
  */
5446
5510
  function appendFindingContext(lines, result, policy) {
5447
- if (result.vacuity?.settled === false) {
5511
+ if (result.vacuity?.evidence?.expired ?? result.vacuity?.settled === false) {
5448
5512
  lines.push(
5449
5513
  "",
5450
5514
  "⏳ The settle wait EXPIRED before every declared review check reached a terminal state, so the readings above are where the wait stopped rather than where the reviewer finished. RE-RUN this job before treating any of it as a finding about the change."
@@ -5602,6 +5666,13 @@ function buildTextReport(result, policy, outcome) {
5602
5666
  const lines = ["## 🔒 Required checks that prove nothing", ""];
5603
5667
  appendVerdictReport(lines, result);
5604
5668
  appendRequiredSetReport(lines, result.vacuity?.requiredSet);
5669
+ if (result.vacuity?.registration !== undefined) {
5670
+ lines.push(
5671
+ result.vacuity.evidence.line,
5672
+ result.vacuity.registration.line,
5673
+ ""
5674
+ );
5675
+ }
5605
5676
  appendSnapshotRefusal(lines, result, policy);
5606
5677
  appendInspectionRefusal(lines, outcome);
5607
5678
  appendOutcomeReport(lines, result.outcomes);
@@ -5622,6 +5693,26 @@ function buildTextReport(result, policy, outcome) {
5622
5693
  * @returns {void}
5623
5694
  */
5624
5695
  function writeTextResult(result, policy, outcome) {
5696
+ if (outcome.failed) {
5697
+ // Emit the actual verdict BEFORE any detailed error, including refusals.
5698
+ const reasons = [
5699
+ ...new Set([
5700
+ ...outcome.blocking.map(violation => violation.kind),
5701
+ ...(outcome.refusalBlocks ? [outcome.refusal.kind] : []),
5702
+ ...(outcome.outcomesRefusal === null
5703
+ ? []
5704
+ : [outcome.outcomesRefusal.kind]),
5705
+ ...(!result.trust.trusted && !policy.warnOnly
5706
+ ? ["required-checks snapshot untrusted"]
5707
+ : []),
5708
+ ]),
5709
+ ];
5710
+ const registration = result.vacuity?.registration;
5711
+ const summary = registration?.expired
5712
+ ? `${registration.line} Blocking verdict: ${reasons.join(", ")}.`
5713
+ : `${outcome.blocking.length} blocking finding(s): ${reasons.join(", ")}.`;
5714
+ process.stderr.write(`::error title=Required checks verdict::${summary}\n`);
5715
+ }
5625
5716
  const report = buildTextReport(result, policy, outcome);
5626
5717
  process.stdout.write(report);
5627
5718
  writeStepSummary(report);
@@ -208,11 +208,11 @@ jobs:
208
208
  # without the install being broken, and a green run that inspected
209
209
  # nothing is the exact defect being checked for.
210
210
  if [ ! -f scripts/check-skipped-required-checks.mjs ]; then
211
- echo "::error title=Vacuity prover missing::scripts/check-skipped-required-checks.mjs does not exist, so no required check was read on this pull request. Run \`lisa apply\` to install the Lisa-managed script."
211
+ printf '::%s title=%s::%s\n' error "Vacuity prover missing" "scripts/check-skipped-required-checks.mjs does not exist, so no required check was read on this pull request. Run \`lisa apply\` to install the Lisa-managed script."
212
212
  exit 1
213
213
  fi
214
214
  if [ ! -f .github/required-checks.json ]; then
215
- echo "::error title=Required-checks declaration missing::.github/required-checks.json does not exist, so nothing names which checks are supposed to carry review evidence. It is create-only and Lisa ships a seed — run \`lisa apply\` to get it."
215
+ printf '::%s title=%s::%s\n' error "Required-checks declaration missing" ".github/required-checks.json does not exist, so nothing names which checks are supposed to carry review evidence. It is create-only and Lisa ships a seed — run \`lisa apply\` to get it."
216
216
  exit 1
217
217
  fi
218
218
  if [ -n "${VACUITY_PR:-}" ]; then