@codyswann/lisa 4.35.2 → 4.35.4

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 (79) 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/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +25 -2
  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 +5 -1
  10. package/plugins/lisa/.codex-plugin/hooks.json +4 -0
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa/hooks/block-host-name-leak.agy.sh +61 -0
  13. package/plugins/lisa/hooks/block-host-name-leak.mjs +412 -0
  14. package/plugins/lisa/hooks/block-host-name-leak.sh +61 -0
  15. package/plugins/lisa-agy/hooks/block-host-name-leak.agy.sh +61 -0
  16. package/plugins/lisa-agy/hooks/block-host-name-leak.mjs +412 -0
  17. package/plugins/lisa-agy/hooks/block-host-name-leak.sh +61 -0
  18. package/plugins/lisa-agy/hooks.json +13 -0
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +5 -1
  26. package/plugins/lisa-copilot/hooks/block-host-name-leak.mjs +412 -0
  27. package/plugins/lisa-copilot/hooks/block-host-name-leak.sh +61 -0
  28. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/hooks/block-host-name-leak.mjs +412 -0
  30. package/plugins/lisa-cursor/hooks/block-host-name-leak.sh +61 -0
  31. package/plugins/lisa-cursor/hooks/hooks.json +4 -0
  32. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-agy/plugin.json +1 -1
  35. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo-cursor/.claude-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-nestjs/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  55. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-agy/plugin.json +1 -1
  60. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  65. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  70. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/src/base/.claude-plugin/plugin.json +4 -0
  73. package/plugins/src/base/hooks/block-host-name-leak.agy.sh +61 -0
  74. package/plugins/src/base/hooks/block-host-name-leak.mjs +412 -0
  75. package/plugins/src/base/hooks/block-host-name-leak.sh +61 -0
  76. package/scripts/downstream-name-digest.mjs +128 -0
  77. package/scripts/generate-agy-plugin-artifacts.mjs +12 -0
  78. package/scripts/reconcile-release-tags.mjs +331 -0
  79. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +132 -39
@@ -1246,30 +1246,47 @@ export const REVIEW_GATE_STATES = Object.freeze({
1246
1246
  * WHICH condition produced the state, as stable tokens.
1247
1247
  *
1248
1248
  * The state is the gate's SEVERITY and stays three-valued; this is what the
1249
- * gate OBSERVED, and it is six-valued because `unsatisfied` is reached from
1250
- * four structurally different situations:
1251
- *
1252
- * | condition | what actually happened | operator's next move |
1253
- * |----------------|-----------------------------------|-----------------------|
1254
- * | `absent` | nobody reviewed | investigate the silence |
1255
- * | `objected` | a review ran and objected | READ THE OBJECTION |
1256
- * | `pending` | the reviewer is late | wait, then re-run |
1257
- * | `unrecognised` | the reviewer said something new | classify the phrase |
1258
- *
1259
- * All four published one word, so an operator handed `unsatisfied` had to guess
1260
- * between four situations whose correct responses have nothing in common — and
1261
- * three of those guesses are wrong. That collapse is why CodySwannGT/lisa#3706,
1262
- * #3716 and #3600 each described a different defect and each was partly right:
1263
- * they are four faces of one impoverished vocabulary.
1249
+ * gate OBSERVED, and it is seven-valued because `unsatisfied` is reached from
1250
+ * five structurally different situations:
1251
+ *
1252
+ * | condition | what actually happened | operator's next move |
1253
+ * |-----------------|----------------------------------|-----------------------|
1254
+ * | `absent` | nobody reviewed | investigate the silence |
1255
+ * | `objected` | a review ran and objected | READ THE OBJECTION |
1256
+ * | `pending` | the reviewer is late | wait, then re-run |
1257
+ * | `unrecognised` | the reviewer said something new | classify the phrase |
1258
+ * | `undetermined` | THE GATE STOPPED WAITING | re-run; read nothing into it |
1259
+ *
1260
+ * All of them published one word, so an operator handed `unsatisfied` had to
1261
+ * guess between situations whose correct responses have nothing in common. That
1262
+ * collapse is why CodySwannGT/lisa#3706, #3716 and #3600 each described a
1263
+ * different defect and each was partly right: they are faces of one
1264
+ * impoverished vocabulary.
1264
1265
  *
1265
1266
  * The second row is the sharpest. This file states that a review which
1266
1267
  * "RAN AND OBJECTED is the case this gate exists to let through to a human" —
1267
1268
  * and then reported it identically to nobody-reviewed. Blocking is correct
1268
1269
  * there, so no exit code was ever wrong; what was missing is any way to tell
1269
- * the operator which of the four they were looking at.
1270
+ * the operator which of them they were looking at.
1271
+ *
1272
+ * THE LAST ROW IS NOT AN OBSERVATION AND MUST NEVER READ LIKE ONE (#3716).
1273
+ * `absent` and `pending` are things the gate SAW; `undetermined` is the gate
1274
+ * saying it ran out of time before it could see anything. Reported as `absent`
1275
+ * — "did not report on this pull request at all... investigate the silence" —
1276
+ * it sends an operator to audit the change instead of the wait, which is
1277
+ * exactly what a false red costs. MEASURED: a settle window expired NINE
1278
+ * SECONDS before the review it was waiting for arrived.
1279
+ *
1280
+ * And it is not a rare corner. {@link checksSettled} returns false whenever a
1281
+ * declared check is missing or still pending, so the wait CANNOT end early on
1282
+ * either shape: with one declared check, every `absent` and every `pending`
1283
+ * this gate can currently produce is a wait that expired. The two tokens are
1284
+ * kept because they are the honest labels for a settled reading, and a future
1285
+ * declaration with several checks can settle one while another is still in
1286
+ * flight — but today `undetermined` is what fires, and that is the point.
1270
1287
  *
1271
1288
  * SEVERITY IS DELIBERATELY UNCHANGED HERE. See `reviewGateState` for why
1272
- * `pending` keeps blocking.
1289
+ * `pending` keeps blocking, and why `undetermined` blocks with it.
1273
1290
  */
1274
1291
  export const REVIEW_GATE_CONDITIONS = Object.freeze({
1275
1292
  satisfied: "satisfied",
@@ -1278,6 +1295,7 @@ export const REVIEW_GATE_CONDITIONS = Object.freeze({
1278
1295
  objected: "objected",
1279
1296
  pending: "pending",
1280
1297
  unrecognised: "unrecognised",
1298
+ undetermined: "undetermined",
1281
1299
  });
1282
1300
 
1283
1301
  /**
@@ -1317,17 +1335,35 @@ function normalizeDescription(description) {
1317
1335
  * would trade a false red for a silent merge, and this file has a whole
1318
1336
  * vocabulary of evidence that the silent direction is the expensive one.
1319
1337
  *
1320
- * @param {{present: boolean, state?: string, description?: string}} reading - One check
1338
+ * WAITING IS NOT OBSERVING (#3716). `reading.waitExpired` is the settle loop
1339
+ * reporting that it hit its deadline rather than reaching a terminal read. When
1340
+ * it is set, the two shapes that mean "nothing conclusive yet" — no status at
1341
+ * all, and a status still in flight — are reported as `undetermined` instead of
1342
+ * as `absent` and `pending`. The distinction between those two shapes is
1343
+ * preserved inside the sentence, because "never started" and "started and ran
1344
+ * long" are different things to go and look at; what changes is that neither is
1345
+ * asserted as a fact about the reviewer when the gate simply stopped waiting.
1346
+ *
1347
+ * @param {{present: boolean, state?: string, description?: string, waitExpired?: boolean}} reading - One check, and whether the settle wait expired
1321
1348
  * @param {{waive?: readonly string[], satisfy?: readonly string[]}} [vocabulary] - Per-check extensions
1322
1349
  * @returns {{state: string, condition: string, why: string}} Severity, the condition observed, and a one-line reason
1323
1350
  */
1324
1351
  export function reviewGateState(reading, vocabulary = {}) {
1352
+ // Shared by both inconclusive shapes so they can never drift apart, and so
1353
+ // the sentence always says what to do (re-run) before it says what was seen.
1354
+ const undetermined = seen => ({
1355
+ state: REVIEW_GATE_STATES.unsatisfied,
1356
+ condition: REVIEW_GATE_CONDITIONS.undetermined,
1357
+ why: `was ${seen} when the settle window EXPIRED. The gate stopped waiting; it did not observe that nobody reviewed. This is NOT a finding about the code and nothing about the change should be investigated on the strength of it — RE-RUN THIS JOB once the reviewer has settled. Blocking is deliberate: an expired wait has not established that a review happened, and passing on it would let a pull request merge unreviewed whenever a reviewer ran slow. Do NOT re-request the review: a re-request OVERWRITES the existing commit status rather than adding to it, so under throttle it destroys a real review one-way and replaces a substantive objection with a rate-limit string.`,
1358
+ });
1325
1359
  if (!reading.present) {
1326
- return {
1327
- state: REVIEW_GATE_STATES.unsatisfied,
1328
- condition: REVIEW_GATE_CONDITIONS.absent,
1329
- why: "did not report on this pull request at all. ABSENT is not the same as waived: nothing said it could not review, so nothing accounts for the silence. A guard that read absence as permission would pass forever the first time it looked at the wrong commit.",
1330
- };
1360
+ return reading.waitExpired === true
1361
+ ? undetermined("still unreported")
1362
+ : {
1363
+ state: REVIEW_GATE_STATES.unsatisfied,
1364
+ condition: REVIEW_GATE_CONDITIONS.absent,
1365
+ why: "did not report on this pull request at all. ABSENT is not the same as waived: nothing said it could not review, so nothing accounts for the silence. A guard that read absence as permission would pass forever the first time it looked at the wrong commit.",
1366
+ };
1331
1367
  }
1332
1368
  const state = String(reading.state ?? "").toUpperCase();
1333
1369
  const text = normalizeDescription(reading.description);
@@ -1339,11 +1375,13 @@ export function reviewGateState(reading, vocabulary = {}) {
1339
1375
  };
1340
1376
  }
1341
1377
  if (state !== "SUCCESS") {
1342
- return {
1343
- state: REVIEW_GATE_STATES.unsatisfied,
1344
- condition: REVIEW_GATE_CONDITIONS.pending,
1345
- why: `is still ${state === "" ? "unreported" : state} after the settle window. An unsettled check has not said anything yet, and the gate does not guess on its behalf. This is LATENESS, not a verdict about the code — wait for the reviewer to settle, then RE-RUN THIS JOB. Do NOT re-request the review: a re-request OVERWRITES the existing commit status rather than adding to it, so under throttle it destroys a real review one-way and replaces a substantive objection with a rate-limit string.`,
1346
- };
1378
+ return reading.waitExpired === true
1379
+ ? undetermined(`still ${state === "" ? "unreported" : state}`)
1380
+ : {
1381
+ state: REVIEW_GATE_STATES.unsatisfied,
1382
+ condition: REVIEW_GATE_CONDITIONS.pending,
1383
+ why: `is still ${state === "" ? "unreported" : state} after the settle window. An unsettled check has not said anything yet, and the gate does not guess on its behalf. This is LATENESS, not a verdict about the code — wait for the reviewer to settle, then RE-RUN THIS JOB. Do NOT re-request the review: a re-request OVERWRITES the existing commit status rather than adding to it, so under throttle it destroys a real review one-way and replaces a substantive objection with a rate-limit string.`,
1384
+ };
1347
1385
  }
1348
1386
  const satisfies = [...REVIEW_SATISFACTIONS, ...(vocabulary.satisfy ?? [])];
1349
1387
  if (satisfies.some(phrase => text === normalizeDescription(phrase))) {
@@ -1378,14 +1416,19 @@ export function reviewGateState(reading, vocabulary = {}) {
1378
1416
  *
1379
1417
  * @param {object} declaration - The per-repo declaration
1380
1418
  * @param {ReadonlyArray<{name: string, state: string, description?: string}>} checks - The checks
1381
- * @param {{headSha?: string}} [options] - `headSha` is cited in every finding
1382
- * @returns {{violations: object[], states: Record<string, string>, descriptions: Record<string, string>, checked: number}} Findings, per-check state, the description each verdict was read from, and how many were examined
1419
+ * @param {{headSha?: string, waitExpired?: boolean}} [options] - `headSha` is cited in every finding; `waitExpired` says the settle loop hit its deadline
1420
+ * @returns {{violations: object[], states: Record<string, string>, conditions: Record<string, string>, descriptions: Record<string, string>, checked: number}} Findings, per-check state and condition, the description each verdict was read from, and how many were examined
1383
1421
  */
1384
1422
  export function evaluateReviewGate(declaration, checks, options = {}) {
1385
1423
  const declared = declaration.evidence_bearing_checks ?? {};
1386
1424
  const at = citeHeadSha(options.headSha);
1387
1425
  const violations = [];
1388
1426
  const states = {};
1427
+ // Carried alongside the states for the same reason the descriptions are: the
1428
+ // RENDERED verdict has to say WHICH condition it is publishing, and `states`
1429
+ // is three-valued severity that cannot tell an expired wait from a review
1430
+ // that ran and objected (#3716).
1431
+ const conditions = {};
1389
1432
  // Carried out with the states because the RENDERED verdict quotes it. A title
1390
1433
  // that said only "WAIVED" would tell an operator that something was waived
1391
1434
  // and not which vendor sentence did the waiving, which is the fact that says
@@ -1399,15 +1442,17 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
1399
1442
  const found = checks.find(check => check.name === name);
1400
1443
  const verdict = reviewGateState(
1401
1444
  found === undefined
1402
- ? { present: false }
1445
+ ? { present: false, waitExpired: options.waitExpired === true }
1403
1446
  : {
1404
1447
  present: true,
1405
1448
  state: found.state,
1406
1449
  description: found.description,
1450
+ waitExpired: options.waitExpired === true,
1407
1451
  },
1408
1452
  vocabulary
1409
1453
  );
1410
1454
  states[name] = verdict.state;
1455
+ conditions[name] = verdict.condition;
1411
1456
  // Assigned ONLY when the check reported. An absent check with a `""`
1412
1457
  // description entry renders as `CodeRabbit reported ""` — a sentence that
1413
1458
  // says it reported. MEASURED on this fix's own first commit, where the
@@ -1432,7 +1477,7 @@ export function evaluateReviewGate(declaration, checks, options = {}) {
1432
1477
  });
1433
1478
  }
1434
1479
 
1435
- return { violations, states, descriptions, checked };
1480
+ return { violations, states, conditions, descriptions, checked };
1436
1481
  }
1437
1482
 
1438
1483
  /**
@@ -1512,13 +1557,22 @@ function waiveRateSuffix(rate) {
1512
1557
  * that matters — a waiver and a satisfaction never render the same — is
1513
1558
  * testable without a GitHub API.
1514
1559
  *
1515
- * @param {{states?: Record<string, string>, descriptions?: Record<string, string>, refusal?: {kind: string}|null, waiveRate?: {waived: number, sampled: number}}} reading -
1516
- * The gate's per-check states, the descriptions they were read from, any
1517
- * refusal, and an optional sampled waive rate
1560
+ * THE TITLE IS THE ONLY LAYER A MERGE DECISION READS (#3716). Before this, an
1561
+ * expired settle wait published `UNREVIEWED — ... posted NO review status at
1562
+ * all`: a sentence asserting an observation the gate never made. The timeout
1563
+ * was known — the settle loop returned it — and it reached only a parenthetical
1564
+ * in the job summary, and only on the branch where there were NO violations. So
1565
+ * the one path where it mattered, a red check blocking a merge, was the one
1566
+ * path that never said it. The conditions are carried here for that reason.
1567
+ *
1568
+ * @param {{states?: Record<string, string>, conditions?: Record<string, string>, descriptions?: Record<string, string>, refusal?: {kind: string}|null, waiveRate?: {waived: number, sampled: number}}} reading -
1569
+ * The gate's per-check states and conditions, the descriptions they were read
1570
+ * from, any refusal, and an optional sampled waive rate
1518
1571
  * @returns {{verdict: string, conclusion: string, title: string}} What to publish
1519
1572
  */
1520
1573
  export function reviewGateVerdict(reading = {}) {
1521
1574
  const states = reading.states ?? {};
1575
+ const conditions = reading.conditions ?? {};
1522
1576
  const descriptions = reading.descriptions ?? {};
1523
1577
  const names = Object.keys(states);
1524
1578
  const suffix = waiveRateSuffix(reading.waiveRate);
@@ -1540,23 +1594,45 @@ export function reviewGateVerdict(reading = {}) {
1540
1594
  * check that posted nothing is the one phrasing this whole file exists to
1541
1595
  * refuse: it makes absence look like a quiet answer.
1542
1596
  *
1597
+ * An `undetermined` check is quoted as a WAIT, never as a report. Saying it
1598
+ * "posted NO review status at all" is true of the bytes and false as an
1599
+ * account of what happened, and it is the sentence that sends an operator to
1600
+ * audit the change rather than re-run the job.
1601
+ *
1543
1602
  * @param {string} name - The declared check name
1544
- * @returns {string} `<name> reported "<description>"`, or that it posted none
1603
+ * @returns {string} `<name> reported "<description>"`, or what the wait did
1545
1604
  */
1546
- const quote = name =>
1547
- Object.hasOwn(descriptions, name)
1605
+ const quote = name => {
1606
+ if (conditions[name] === REVIEW_GATE_CONDITIONS.undetermined) {
1607
+ return `${name} had not settled when the gate's wait EXPIRED — not observed to be unreviewed`;
1608
+ }
1609
+ return Object.hasOwn(descriptions, name)
1548
1610
  ? `${name} reported ${JSON.stringify(descriptions[name])}`
1549
1611
  : `${name} posted NO review status at all`;
1612
+ };
1550
1613
 
1551
1614
  const unsatisfied = names.filter(
1552
1615
  name => states[name] === REVIEW_GATE_STATES.unsatisfied
1553
1616
  );
1554
1617
  if (unsatisfied.length > 0) {
1618
+ // FAIL-CLOSED, LABELLED. The conclusion is `failure` either way: an expired
1619
+ // wait has not established that a review happened, and the whole family of
1620
+ // defects this gate is made of is a control reporting a conclusion it did
1621
+ // not reach — so resolving a timeout to `success` would be a new instance
1622
+ // of exactly that. What changes is the WORD, because "we could not tell"
1623
+ // and "nobody reviewed" are different things to hand a human. Only when
1624
+ // EVERY unsatisfied check is undetermined, so a real objection alongside a
1625
+ // slow reviewer still leads with UNREVIEWED.
1626
+ const allUndetermined = unsatisfied.every(
1627
+ name => conditions[name] === REVIEW_GATE_CONDITIONS.undetermined
1628
+ );
1555
1629
  return {
1556
1630
  verdict: REVIEW_GATE_STATES.unsatisfied,
1557
1631
  conclusion: REVIEW_VERDICT_CONCLUSIONS.unsatisfied,
1558
1632
  title: fitTitle(
1559
- `UNREVIEWED — review evidence unsatisfied: ${unsatisfied.map(quote).join("; ")}${suffix}`
1633
+ allUndetermined
1634
+ ? `UNDETERMINED — the gate stopped waiting before review evidence settled, and is NOT reporting that nobody reviewed: ${unsatisfied.map(quote).join("; ")}. RE-RUN this job; do not investigate the change on the strength of this${suffix}`
1635
+ : `UNREVIEWED — review evidence unsatisfied: ${unsatisfied.map(quote).join("; ")}${suffix}`
1560
1636
  ),
1561
1637
  };
1562
1638
  }
@@ -2444,6 +2520,11 @@ export function inspectVacuity(argv, declaration, options = {}) {
2444
2520
  // repositories that have not opted in yet.
2445
2521
  const gate = evaluateReviewGate(declaration, read.checks, {
2446
2522
  headSha: read.headSha,
2523
+ // The settle loop exits on one of two things: everything declared reached a
2524
+ // terminal read, or the deadline passed. So `settled === false` IS "the
2525
+ // wait expired", and it is the only place that fact exists — every reader
2526
+ // downstream sees a check row that looks identical either way (#3716).
2527
+ waitExpired: read.settled === false,
2447
2528
  });
2448
2529
  // Sampled ONLY when this pull request is itself waived. The rate answers
2449
2530
  // "is this the exception or the rule?", a question that only arises once a
@@ -2466,10 +2547,12 @@ export function inspectVacuity(argv, declaration, options = {}) {
2466
2547
  checked: evaluated.checked,
2467
2548
  violations: [...evaluated.violations, ...gate.violations],
2468
2549
  gateStates: gate.states,
2550
+ gateConditions: gate.conditions,
2469
2551
  gateDescriptions: gate.descriptions,
2470
2552
  waiveRate,
2471
2553
  verdict: reviewGateVerdict({
2472
2554
  states: gate.states,
2555
+ conditions: gate.conditions,
2473
2556
  descriptions: gate.descriptions,
2474
2557
  waiveRate,
2475
2558
  }),
@@ -2712,6 +2795,16 @@ function main(argv) {
2712
2795
  `::${blocks(violation) ? "error" : "warning"} title=${violation.kind}::${violation.message.split("\n")[0]}\n`
2713
2796
  );
2714
2797
  }
2798
+ // The clean branch above has always disclosed an expired wait. This branch
2799
+ // — a RED run, the only one where the disclosure changes what an operator
2800
+ // does next — did not, so the fact was published exactly where it was not
2801
+ // needed and withheld where it was (#3716).
2802
+ if (result.vacuity?.settled === false) {
2803
+ lines.push(
2804
+ "",
2805
+ "⏳ 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."
2806
+ );
2807
+ }
2715
2808
  if (warnOnly) {
2716
2809
  lines.push(
2717
2810
  "",