@codyswann/lisa 4.10.1 → 4.10.3

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 (64) hide show
  1. package/dist/codex/scripts/setup-jira-cli.sh +87 -18
  2. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.js +3 -0
  4. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +4 -2
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/dist/opencode/plugin-templates/lisa-session-bootstrap.ts +11 -2
  9. package/package.json +1 -1
  10. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa/hooks/setup-jira-cli.sh +88 -22
  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-copilot/hooks/setup-jira-cli.sh +88 -22
  21. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cursor/hooks/setup-jira-cli.sh +88 -22
  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-harper-fabric/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  36. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  41. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  46. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails-agy/plugin.json +1 -1
  51. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  56. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  61. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/src/base/hooks/setup-jira-cli.sh +88 -22
  64. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +321 -10
@@ -10,7 +10,8 @@
10
10
  * halves implement is `docs/nightly-e2e-gate.md` in Lisa, whose §2 truth table
11
11
  * is proven row-by-row by `tests/unit/scripts/nightly-e2e-health*.test.ts`
12
12
  * (rows 1-16, `-api` rows 17-20, `-bypass` rows 21-25, `-completeness` row 26,
13
- * `-issues` rows 27-31).
13
+ * `-issues` rows 27-31), with §10.9 proven by
14
+ * `tests/unit/scripts/nightly-e2e-health-bypass-label.test.ts`.
14
15
  *
15
16
  * Usage:
16
17
  * node scripts/check-nightly-e2e-health.mjs # human report, exit 1 when blocked
@@ -79,6 +80,22 @@
79
80
  * instead, which also means a label somebody REMOVED stops waiving. An
80
81
  * unreadable pull request is a REJECTED bypass, never a granted one.
81
82
  *
83
+ * ## The escape hatch has to EXIST (contract 1.7.0, §10.9)
84
+ *
85
+ * Row 40's other half. There the label existed and the gate could not see it;
86
+ * here there is no label to apply. Measured across four adopters on 2026-08-19,
87
+ * two had the gate required by an active ruleset and no bypass label at all — so
88
+ * §6's preferred path was unreachable and the unaudited admin merge was the only
89
+ * exit, reached by following the printed instructions exactly.
90
+ *
91
+ * `fetchBypassLabelState` measures it on the REPORTING half, nightly, whether or
92
+ * not anybody is blocked yet: discovering the escape hatch is absent while
93
+ * trying to use it is the failure mode, so the gate half — which only speaks
94
+ * once somebody is already blocked — is the wrong place to find out. An
95
+ * unreadable labels API is `unknown`, never `present`; nothing here creates the
96
+ * label, because manufacturing a bypass surface in a repository that never
97
+ * adopted §6 is a new hole rather than a closed one.
98
+ *
82
99
  * ## Inherited from three implementations, with one path closed
83
100
  *
84
101
  * `DECISIVE_CONCLUSIONS` comes from acmeorgb's `check-nightly-e2e.mjs` and is
@@ -101,7 +118,7 @@ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
101
118
  * rather than running a contract neither half agrees on. See §8 of
102
119
  * `docs/nightly-e2e-gate.md` for what counts as major / minor / patch.
103
120
  */
104
- export const NIGHTLY_E2E_CONTRACT_VERSION = "1.6.0";
121
+ export const NIGHTLY_E2E_CONTRACT_VERSION = "1.7.0";
105
122
 
106
123
  /**
107
124
  * The conclusions that constitute a verdict about the code.
@@ -267,6 +284,35 @@ export const REQUIREDNESS = Object.freeze({
267
284
  unknown: "unknown",
268
285
  });
269
286
 
287
+ /**
288
+ * Whether the audited bypass label EXISTS — measured, never assumed (§10.9).
289
+ *
290
+ * §6 names the `nightly-e2e-bypass` label the preferred way past a red gate.
291
+ * That path only exists if the label exists. Measured across four adopters on
292
+ * 2026-08-19: two had the gate required by an active ruleset and **no bypass
293
+ * label at all**, so the documented escape hatch was unreachable and the only
294
+ * remaining exit was the unaudited admin merge — precisely the outcome §6
295
+ * exists to prevent. A gate whose printed remedy cannot be followed is worse
296
+ * than one with no remedy, because an operator follows the instructions and
297
+ * they do not work.
298
+ *
299
+ * `unknown` is a first-class member for the same reason it is one in
300
+ * `REQUIREDNESS`: an unreadable labels API must never render as either answer.
301
+ * Rendering `present` on an unreadable read would reprint the instruction that
302
+ * does not work; rendering `absent` would file a defect against a repository
303
+ * that is fine. Note which way the asymmetry runs, though — the state this
304
+ * guard must never invent is `present`, because that is the one that claims an
305
+ * escape hatch exists. Nothing here CREATES the label: manufacturing a bypass
306
+ * surface in a repository that never adopted the contract is a different
307
+ * defect, not a fix for this one.
308
+ */
309
+ export const BYPASS_LABEL_STATE = Object.freeze({
310
+ present: "present",
311
+ absent: "absent",
312
+ unknown: "unknown",
313
+ notMeasured: "not_measured",
314
+ });
315
+
270
316
  /**
271
317
  * The status-check context the gate publishes, as Lisa's own caller names it.
272
318
  *
@@ -282,6 +328,18 @@ export const REQUIREDNESS = Object.freeze({
282
328
  */
283
329
  export const DEFAULT_GATE_CONTEXT = "🌙 Nightly E2E Health / 🌙 Gate";
284
330
 
331
+ /**
332
+ * The audited bypass label's default name, as §6 documents it.
333
+ *
334
+ * One constant rather than three literals: this string is the identity of the
335
+ * escape hatch, and §10.9 now MEASURES whether a label by this name exists.
336
+ * Three copies is three chances for the renderer, the settings resolver and the
337
+ * measurement to disagree about which label they are talking about — and a
338
+ * disagreement there reads to an operator as "the label is missing" when it is
339
+ * merely spelled differently in one of them.
340
+ */
341
+ export const DEFAULT_BYPASS_LABEL = "nightly-e2e-bypass";
342
+
285
343
  // ---------------------------------------------------------------------------
286
344
  // SECURITY LIMITS — source constants, never env-readable (portfolio doctrine)
287
345
  // ---------------------------------------------------------------------------
@@ -1895,6 +1953,7 @@ function bypassParagraph(claim, context) {
1895
1953
  "",
1896
1954
  ];
1897
1955
  }
1956
+ const defect = bypassLabelLines(context);
1898
1957
  const recipe = [
1899
1958
  `1. Apply the \`${label}\` label to the pull request.`,
1900
1959
  "2. Put a line in the **pull request body** naming a ticket and a reason:",
@@ -1906,16 +1965,73 @@ function bypassParagraph(claim, context) {
1906
1965
  `3. The waiver is time-boxed and only an \`admin\` or \`maintain\` collaborator can grant one. It is recorded on the run, and the label is removed when the PR closes. This tracking issue stays open either way — a bypass waives ONE pull request, it does not make the nightly green.`,
1907
1966
  ];
1908
1967
  if (claim.state === REQUIREDNESS.required) {
1909
- return ["**If you must merge before this is fixed.**", "", ...recipe, ""];
1968
+ return [
1969
+ "**If you must merge before this is fixed.**",
1970
+ "",
1971
+ ...defect,
1972
+ ...recipe,
1973
+ "",
1974
+ ];
1910
1975
  }
1911
1976
  return [
1912
1977
  `**If you must merge before this is fixed.** ⚠️ Confirm first whether the gate is actually required on \`${context.branch}\` — that could not be read tonight. If it is not, the \`${label}\` label waives nothing and should not be applied. If it is:`,
1913
1978
  "",
1979
+ ...defect,
1914
1980
  ...recipe,
1915
1981
  "",
1916
1982
  ];
1917
1983
  }
1918
1984
 
1985
+ /**
1986
+ * The §10.9 defect block: the escape hatch this gate documents does not exist.
1987
+ *
1988
+ * Rendered ABOVE the waiver recipe rather than instead of it, deliberately. The
1989
+ * recipe is not wrong, it is unreachable — and it becomes reachable the moment
1990
+ * somebody runs the one command printed here. Deleting it would leave a reader
1991
+ * who fixed the label with no instructions for the thing they just enabled.
1992
+ *
1993
+ * Empty when the label is `present`, so a repository that installed the whole
1994
+ * contract reads EXACTLY what it read before this shipped — byte for byte.
1995
+ * That is the control: a defect renderer that also perturbs the healthy case
1996
+ * cannot be told apart from a renderer that fires indiscriminately.
1997
+ *
1998
+ * @param {{bypassLabel: string, bypassLabelState?: {state: string, detail: string|null},
1999
+ * requiredness?: {rulesets?: ReadonlyArray<object>}}} context - Reporting context
2000
+ * @returns {ReadonlyArray<string>} Markdown lines, possibly none
2001
+ */
2002
+ function bypassLabelLines(context) {
2003
+ const label = context.bypassLabel;
2004
+ const measured = context.bypassLabelState;
2005
+ const state = measured?.state ?? BYPASS_LABEL_STATE.unknown;
2006
+ if (state === BYPASS_LABEL_STATE.absent) {
2007
+ return [
2008
+ "> [!CAUTION]",
2009
+ `> **This gate is armed with no escape hatch.** The \`${label}\` label does not exist in this repository, so the audited waiver below **cannot be applied**. The gate is required by ${describeRulesets(context.requiredness?.rulesets)}, which means a red suite holds every pull request into \`${context.branch}\` and the only remaining route past it is an unaudited admin merge — the one route that records nothing about who waived what. Anyone with \`admin\` or \`maintain\` can fix it with one command:`,
2010
+ ">",
2011
+ "> ```",
2012
+ `> gh label create ${label} --description "Audited waiver for the nightly e2e merge gate — docs/nightly-e2e-gate.md section 6" --color B60205`,
2013
+ "> ```",
2014
+ ">",
2015
+ "> This report does not create the label itself. Conjuring a bypass surface into a repository whose owners never chose to have one is a different defect, not a fix for this one — so the mismatch is named and the decision is left where it belongs.",
2016
+ "",
2017
+ ];
2018
+ }
2019
+ // `not_measured` and `present` both render NOTHING, and they are kept apart
2020
+ // anyway: `not_measured` is "nobody asked", `unknown` is "we asked and the
2021
+ // API would not say". Collapsing the first into the second would print a
2022
+ // hedge in every issue filed by a caller that predates this measurement, and
2023
+ // collapsing the second into the first would silently swallow the case the
2024
+ // hedge exists for.
2025
+ if (state === BYPASS_LABEL_STATE.unknown) {
2026
+ return [
2027
+ "> [!WARNING]",
2028
+ `> Whether the \`${label}\` label exists in this repository could not be read${measured?.detail ? ` (${measured.detail})` : ""}. Confirm it exists before relying on the route below — "we could not check" is not "it is there", and following a recipe for a label that is not there ends at the unaudited admin merge.`,
2029
+ "",
2030
+ ];
2031
+ }
2032
+ return [];
2033
+ }
2034
+
1919
2035
  /**
1920
2036
  * The issue body — written for whoever is standing at the gate.
1921
2037
  *
@@ -2002,8 +2118,15 @@ export function planIssueActions(findings, openIssues, context) {
2002
2118
  const resolved = {
2003
2119
  ...context,
2004
2120
  requiredness: context.requiredness ?? { state: REQUIREDNESS.unknown },
2005
- bypassLabel: context.bypassLabel ?? "nightly-e2e-bypass",
2121
+ bypassLabel: context.bypassLabel ?? DEFAULT_BYPASS_LABEL,
2006
2122
  gateContext: context.gateContext ?? DEFAULT_GATE_CONTEXT,
2123
+ // `not_measured`, never `unknown`: a caller that predates §10.9 did not ask
2124
+ // about the label, and printing "we could not read it" for a question
2125
+ // nobody put is a false report about this repository.
2126
+ bypassLabelState: context.bypassLabelState ?? {
2127
+ state: BYPASS_LABEL_STATE.notMeasured,
2128
+ detail: null,
2129
+ },
2007
2130
  };
2008
2131
  return Object.freeze(
2009
2132
  findings.map(finding => {
@@ -2119,12 +2242,47 @@ export function planIssueActions(findings, openIssues, context) {
2119
2242
  );
2120
2243
  }
2121
2244
 
2245
+ /**
2246
+ * The §10.9 line in the nightly job summary — the surface that runs whether or
2247
+ * not anybody needs the escape hatch yet.
2248
+ *
2249
+ * This is the whole point of putting the measurement on the REPORTING half. The
2250
+ * gate half only speaks when somebody is already blocked, which is the moment
2251
+ * it is too late to discover that the documented way out was never installed.
2252
+ * This job runs every night, green or red, and writes here every time.
2253
+ *
2254
+ * Silent on `present` and on `not_measured`, so a healthy repository's summary
2255
+ * is byte-identical to the one it produced before this shipped.
2256
+ *
2257
+ * @param {{branch: string, bypassLabel?: string, requiredness?: object,
2258
+ * bypassLabelState?: {state: string, detail: string|null}}} context - Reporting context
2259
+ * @returns {ReadonlyArray<string>} Markdown lines, possibly none
2260
+ */
2261
+ function bypassLabelSummary(context) {
2262
+ const label = context.bypassLabel ?? DEFAULT_BYPASS_LABEL;
2263
+ const measured = context.bypassLabelState;
2264
+ const state = measured?.state ?? BYPASS_LABEL_STATE.notMeasured;
2265
+ if (state === BYPASS_LABEL_STATE.absent) {
2266
+ return [
2267
+ `⛔ **Defect — this gate is armed with no escape hatch.** \`${label}\` does not exist in this repository, while the gate is required by ${describeRulesets(context.requiredness?.rulesets)}. The audited waiver §6 documents cannot be applied, so the only route past a red suite is an unaudited admin merge. Create the label once: \`gh label create ${label}\`.`,
2268
+ "",
2269
+ ];
2270
+ }
2271
+ if (state === BYPASS_LABEL_STATE.unknown) {
2272
+ return [
2273
+ `⚠️ Whether the \`${label}\` waiver label exists could not be read${measured?.detail ? ` (${measured.detail})` : ""}. Unreadable is not confirmation that it is there.`,
2274
+ "",
2275
+ ];
2276
+ }
2277
+ return [];
2278
+ }
2279
+
2122
2280
  /**
2123
2281
  * Renders the reporting outcome for the job log and summary.
2124
2282
  *
2125
2283
  * @param {ReadonlyArray<object>} results - Output of `applyIssuePlan`
2126
- * @param {{branch: string, requiredness?: object, gateContext?: string}} context -
2127
- * Reporting context
2284
+ * @param {{branch: string, requiredness?: object, gateContext?: string,
2285
+ * bypassLabel?: string, bypassLabelState?: object}} context - Reporting context
2128
2286
  * @returns {string} Markdown
2129
2287
  */
2130
2288
  export function formatIssueReport(results, context) {
@@ -2150,6 +2308,7 @@ export function formatIssueReport(results, context) {
2150
2308
  "",
2151
2309
  requirednessLine,
2152
2310
  "",
2311
+ ...bypassLabelSummary(context),
2153
2312
  ];
2154
2313
  for (const result of results) {
2155
2314
  const where = result.issues.length
@@ -2387,7 +2546,12 @@ export async function fetchAllJobs(api, runId, wait) {
2387
2546
  */
2388
2547
  export async function fetchRequiredness(api, branch, gateContext, wait) {
2389
2548
  const unknown = detail =>
2390
- Object.freeze({ state: REQUIREDNESS.unknown, detail, contexts: [] });
2549
+ Object.freeze({
2550
+ state: REQUIREDNESS.unknown,
2551
+ detail,
2552
+ contexts: [],
2553
+ rulesets: [],
2554
+ });
2391
2555
  // No context, no measurement. `resolveSettings` can no longer produce an
2392
2556
  // empty one, but this is the place where an empty one does its damage, and a
2393
2557
  // guard belongs where the harm lands as well as where it originates: matching
@@ -2423,9 +2587,11 @@ export async function fetchRequiredness(api, branch, gateContext, wait) {
2423
2587
  "the branch-rules API returned something that is not a list of rules"
2424
2588
  );
2425
2589
  }
2590
+ const rules = result.body.filter(
2591
+ rule => rule?.type === "required_status_checks"
2592
+ );
2426
2593
  const contexts = Object.freeze(
2427
- result.body
2428
- .filter(rule => rule?.type === "required_status_checks")
2594
+ rules
2429
2595
  .flatMap(rule => rule?.parameters?.required_status_checks ?? [])
2430
2596
  .map(check => check?.context)
2431
2597
  .filter(context => typeof context === "string")
@@ -2433,6 +2599,33 @@ export async function fetchRequiredness(api, branch, gateContext, wait) {
2433
2599
  const matched = contexts.filter(context =>
2434
2600
  contextMatchesGate(context, gateContext)
2435
2601
  );
2602
+ // WHICH ruleset requires it, not merely that something does. A defect report
2603
+ // that names only the label leaves the reader hunting for the rule to read;
2604
+ // §10.9 has to name both halves of the mismatch, and this endpoint already
2605
+ // carries the source on every rule it returns. An org-level ruleset renders
2606
+ // as `Organization` here, which is the case where "check your repository
2607
+ // settings" would send someone to the wrong page entirely.
2608
+ const rulesets = Object.freeze(
2609
+ rules
2610
+ .filter(rule =>
2611
+ (rule?.parameters?.required_status_checks ?? []).some(check =>
2612
+ contextMatchesGate(check?.context, gateContext)
2613
+ )
2614
+ )
2615
+ .map(rule =>
2616
+ Object.freeze({
2617
+ sourceType:
2618
+ typeof rule?.ruleset_source_type === "string"
2619
+ ? rule.ruleset_source_type
2620
+ : null,
2621
+ source:
2622
+ typeof rule?.ruleset_source === "string"
2623
+ ? rule.ruleset_source
2624
+ : null,
2625
+ id: typeof rule?.ruleset_id === "number" ? rule.ruleset_id : null,
2626
+ })
2627
+ )
2628
+ );
2436
2629
  return Object.freeze({
2437
2630
  state:
2438
2631
  matched.length > 0 ? REQUIREDNESS.required : REQUIREDNESS.notRequired,
@@ -2441,9 +2634,95 @@ export async function fetchRequiredness(api, branch, gateContext, wait) {
2441
2634
  ? `required as ${matched.map(context => `\`${context}\``).join(", ")}`
2442
2635
  : null,
2443
2636
  contexts,
2637
+ rulesets,
2444
2638
  });
2445
2639
  }
2446
2640
 
2641
+ /**
2642
+ * Renders the ruleset(s) that require this gate, for a defect report.
2643
+ *
2644
+ * @param {ReadonlyArray<{sourceType: string|null, source: string|null, id: number|null}>} rulesets -
2645
+ * Matching rule sources from `fetchRequiredness`
2646
+ * @returns {string} A prose fragment, never empty
2647
+ */
2648
+ export function describeRulesets(rulesets) {
2649
+ const named = (rulesets ?? [])
2650
+ .map(entry => {
2651
+ const scope = entry?.sourceType
2652
+ ? `${entry.sourceType} ruleset`
2653
+ : "ruleset";
2654
+ const name = entry?.source ? ` \`${entry.source}\`` : "";
2655
+ const id = typeof entry?.id === "number" ? ` (id ${entry.id})` : "";
2656
+ return `${scope}${name}${id}`;
2657
+ })
2658
+ .filter(Boolean);
2659
+ // Not "unknown ruleset": the requiredness measurement already proved a rule
2660
+ // is in effect, and the caller only reaches this on `required`. What is
2661
+ // missing is the rule's SOURCE, which older API responses omit.
2662
+ if (named.length === 0) return "an active ruleset on this branch";
2663
+ return named.join(" and ");
2664
+ }
2665
+
2666
+ /**
2667
+ * Measures whether the audited bypass label EXISTS in this repository (§10.9).
2668
+ *
2669
+ * REPORTING ONLY, and it cannot fail its caller — same contract as
2670
+ * `fetchRequiredness`, for the same §10.4 reason: a reporter that aborted on an
2671
+ * unreadable labels API would stop filing the issues that tell people the suite
2672
+ * is down.
2673
+ *
2674
+ * ## Why a 404 is `absent` here, when a 404 is `unknown` for the branch rules
2675
+ *
2676
+ * It looks like the same ambiguity and it is not. `apiGet` maps 404 to `null`,
2677
+ * and `/repos/{o}/{r}/rules/branches/{b}` 404s for a repository the token
2678
+ * cannot see — so there, 404 conflates "no rules" with "not allowed to look".
2679
+ * `/repos/{o}/{r}/labels/{name}` 404s for a label that does not exist in a
2680
+ * repository that IS readable, and the only caller acts on this measurement
2681
+ * after `fetchRequiredness` returned `required`, which required a successful
2682
+ * `200` from the same repository. Repository visibility is therefore already
2683
+ * PROVEN by the time `absent` can be rendered; the remaining meaning of a 404
2684
+ * is the label.
2685
+ *
2686
+ * Everything else — a throw out of `apiGet`, a non-2xx that outlived its
2687
+ * retries, a network failure — is `unknown`. Never `present`: the state this
2688
+ * function must not invent is the one that claims an escape hatch exists. It
2689
+ * never returns `not_measured` either — that token belongs to callers who did
2690
+ * not ask, and this function is the asking.
2691
+ *
2692
+ * @param {object} api - API coordinates
2693
+ * @param {string} label - The configured bypass label
2694
+ * @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
2695
+ * @returns {Promise<{state: string, detail: string|null}>} The measurement, never a throw
2696
+ */
2697
+ export async function fetchBypassLabelState(api, label, wait) {
2698
+ const unknown = detail =>
2699
+ Object.freeze({ state: BYPASS_LABEL_STATE.unknown, detail });
2700
+ if (typeof label !== "string" || label.trim().length === 0) {
2701
+ return unknown(
2702
+ "no bypass label is configured, so there is nothing to look for — an unnamed label is not evidence that a waiver route exists"
2703
+ );
2704
+ }
2705
+ let result;
2706
+ try {
2707
+ result = await apiGet(
2708
+ api,
2709
+ `/repos/${api.repo}/labels/${encodeURIComponent(label)}`,
2710
+ wait
2711
+ );
2712
+ } catch (error) {
2713
+ return unknown(
2714
+ `the labels API was unreadable: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`
2715
+ );
2716
+ }
2717
+ if (result === null) {
2718
+ return Object.freeze({
2719
+ state: BYPASS_LABEL_STATE.absent,
2720
+ detail: `\`GET /repos/${api.repo}/labels/${label}\` returned 404 — no label by that name exists in this repository`,
2721
+ });
2722
+ }
2723
+ return Object.freeze({ state: BYPASS_LABEL_STATE.present, detail: null });
2724
+ }
2725
+
2447
2726
  /**
2448
2727
  * The artifact NAMES a run published, paginated to exhaustion.
2449
2728
  *
@@ -3098,7 +3377,7 @@ export function resolveSettings(env) {
3098
3377
  freshnessHours: limits.freshnessHours,
3099
3378
  bootstrapUntil: env.NIGHTLY_BOOTSTRAP_UNTIL || "",
3100
3379
  bootstrapMaxDays: limits.bootstrapMaxDays,
3101
- bypassLabel: env.NIGHTLY_BYPASS_LABEL || "nightly-e2e-bypass",
3380
+ bypassLabel: env.NIGHTLY_BYPASS_LABEL || DEFAULT_BYPASS_LABEL,
3102
3381
  bypassMaxHours: limits.bypassMaxHours,
3103
3382
  // An ADDITIONAL project rule, never a replacement — see `evaluateBypass`.
3104
3383
  extraBypassReasonPattern: env.NIGHTLY_BYPASS_REASON_PATTERN || "",
@@ -3262,6 +3541,21 @@ export async function runReport(env, wait) {
3262
3541
  settings.gateContext,
3263
3542
  wait
3264
3543
  );
3544
+ // Measured only where a waiver recipe is actually printed. On a branch this
3545
+ // gate does not guard, the issue already says "you do not need a waiver" and
3546
+ // never names the label — so a missing label there gates nothing, and
3547
+ // reporting it would be a defect against a repository that has none. Skipping
3548
+ // the CALL rather than suppressing the RENDER is deliberate: it makes
3549
+ // "not reported when the gate is unrequired" a property of the wire, which a
3550
+ // renderer edit cannot quietly undo.
3551
+ const bypassLabelState =
3552
+ requiredness.state === REQUIREDNESS.notRequired
3553
+ ? Object.freeze({
3554
+ state: BYPASS_LABEL_STATE.notMeasured,
3555
+ detail:
3556
+ "not measured — this gate is not required on this branch, so no waiver recipe is printed and a missing label waives nothing",
3557
+ })
3558
+ : await fetchBypassLabelState(settings.api, settings.bypassLabel, wait);
3265
3559
  const context = {
3266
3560
  branch: settings.branch,
3267
3561
  label: settings.issueLabel,
@@ -3269,6 +3563,7 @@ export async function runReport(env, wait) {
3269
3563
  requiredness,
3270
3564
  gateContext: settings.gateContext,
3271
3565
  bypassLabel: settings.bypassLabel,
3566
+ bypassLabelState,
3272
3567
  pinIssues: settings.pinIssues,
3273
3568
  };
3274
3569
  const plan = planIssueActions(
@@ -3279,6 +3574,7 @@ export async function runReport(env, wait) {
3279
3574
  return {
3280
3575
  findings,
3281
3576
  requiredness,
3577
+ bypassLabelState,
3282
3578
  plan,
3283
3579
  results: await applyIssuePlan(settings.api, plan, wait),
3284
3580
  settings,
@@ -3317,6 +3613,8 @@ async function reportIssues(asJson) {
3317
3613
  branch: settings.branch,
3318
3614
  requiredness: machine.requiredness,
3319
3615
  gateContext: settings.gateContext,
3616
+ bypassLabel: settings.bypassLabel,
3617
+ bypassLabelState: machine.bypassLabelState,
3320
3618
  });
3321
3619
  process.stdout.write(report);
3322
3620
  await appendSummary(report);
@@ -3329,6 +3627,19 @@ async function reportIssues(asJson) {
3329
3627
  `::warning title=Nightly E2E requiredness unknown::Could not read \`${settings.branch}\`'s branch rules, so the tracking issues claim neither that merges are blocked nor that they are not. ${machine.requiredness.detail ?? ""}\n`
3330
3628
  );
3331
3629
  }
3630
+ // §10.9. An annotation rather than a failure, and that is not timidity: this
3631
+ // job's status answers "did REPORTING work" (§10.4), and reddening it for a
3632
+ // repository-configuration defect would teach operators to ignore the one job
3633
+ // that tells them a suite is down. It is loud where loudness is free.
3634
+ if (machine.bypassLabelState?.state === BYPASS_LABEL_STATE.absent) {
3635
+ process.stderr.write(
3636
+ `::error title=Nightly E2E bypass label missing::The gate is required on \`${settings.branch}\` but the \`${settings.bypassLabel}\` label does not exist in this repository, so the audited waiver cannot be applied and an unaudited admin merge is the only exit. Create it once: gh label create ${settings.bypassLabel}\n`
3637
+ );
3638
+ } else if (machine.bypassLabelState?.state === BYPASS_LABEL_STATE.unknown) {
3639
+ process.stderr.write(
3640
+ `::warning title=Nightly E2E bypass label unreadable::Could not read whether \`${settings.bypassLabel}\` exists in this repository. ${machine.bypassLabelState.detail ?? ""}\n`
3641
+ );
3642
+ }
3332
3643
  for (const result of machine.results) {
3333
3644
  for (const warning of result.warnings ?? []) {
3334
3645
  process.stderr.write(