@codyswann/lisa 4.6.4 → 4.6.6

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 (61) hide show
  1. package/dist/cli/doctor-skip-jobs-migration.d.ts.map +1 -1
  2. package/dist/cli/doctor-skip-jobs-migration.js +119 -13
  3. package/dist/cli/doctor-skip-jobs-migration.js.map +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  5. package/dist/core/lisa-owned-hash-ledger.js +1 -0
  6. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +2 -1
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/package.json +1 -1
  11. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  17. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo-agy/plugin.json +1 -1
  24. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  44. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails-agy/plugin.json +1 -1
  49. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  54. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  59. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  61. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +167 -27
@@ -67,6 +67,18 @@
67
67
  * expiry timestamp on screen. Bootstrap forgives absence of evidence, never
68
68
  * evidence of failure — a red run is red inside the window too.
69
69
  *
70
+ * ## The bypass reads the pull request LIVE (contract 1.6.0, row 40)
71
+ *
72
+ * `github.event.pull_request` is a SNAPSHOT taken when the run was triggered,
73
+ * and a re-run replays it verbatim. Reading the bypass label and the
74
+ * `Nightly-E2E-Bypass:` trailer out of it made the gate's own printed remedy
75
+ * impossible to follow: applying the label fired no run, and re-running — the
76
+ * obvious next move — replayed a payload from before the label existed. The
77
+ * waiver was real, correctly recorded, and invisible to the gate that asked for
78
+ * it. `fetchPullRequestState` reads both halves from the API at gate time
79
+ * instead, which also means a label somebody REMOVED stops waiving. An
80
+ * unreadable pull request is a REJECTED bypass, never a granted one.
81
+ *
70
82
  * ## Inherited from three implementations, with one path closed
71
83
  *
72
84
  * `DECISIVE_CONCLUSIONS` comes from acmeorgb's `check-nightly-e2e.mjs` and is
@@ -89,7 +101,7 @@ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
89
101
  * rather than running a contract neither half agrees on. See §8 of
90
102
  * `docs/nightly-e2e-gate.md` for what counts as major / minor / patch.
91
103
  */
92
- export const NIGHTLY_E2E_CONTRACT_VERSION = "1.5.0";
104
+ export const NIGHTLY_E2E_CONTRACT_VERSION = "1.6.0";
93
105
 
94
106
  /**
95
107
  * The conclusions that constitute a verdict about the code.
@@ -1381,6 +1393,8 @@ const BYPASS_REJECTIONS = Object.freeze({
1381
1393
  "it was applied longer ago than `bypass_max_hours` allows. Bypasses auto-expire so a label nobody removes cannot become a permanent hole.",
1382
1394
  no_reason_or_ticket:
1383
1395
  "the PR body carries no `Nightly-E2E-Bypass: <TICKET> <reason>` line. A bypass without a reason and a ticket is not auditable.",
1396
+ pr_state_unreadable:
1397
+ "this pull request's live labels and body could not be read, so whether a waiver was requested is UNKNOWN. The gate stays closed: a bypass that fires when it could not read the request is worse than no bypass. On a private repository the caller job needs `pull-requests: read`; otherwise this is a transient API failure and a re-run resolves it.",
1384
1398
  });
1385
1399
 
1386
1400
  // ---------------------------------------------------------------------------
@@ -1591,8 +1605,17 @@ export function formatReport(verdict, context) {
1591
1605
  );
1592
1606
  } else if (verdict.blocked) {
1593
1607
  if (verdict.bypass && !verdict.bypass.valid) {
1608
+ // `pr_state_unreadable` gets its own lead because the usual one asserts
1609
+ // something this branch cannot know. When the live read failed, whether a
1610
+ // label is present is precisely the unanswered question — saying "a label
1611
+ // is present but was REJECTED" would send the reader to remove a label
1612
+ // that may not exist.
1613
+ const lead =
1614
+ verdict.bypass.reason === "pr_state_unreadable"
1615
+ ? "⛔ **The bypass could not be evaluated**"
1616
+ : `⛔ **A \`${context.bypassLabel}\` label is present but was REJECTED**`;
1594
1617
  lines.push(
1595
- `⛔ **A \`${context.bypassLabel}\` label is present but was REJECTED** — ${BYPASS_REJECTIONS[verdict.bypass.reason] ?? verdict.bypass.reason}`,
1618
+ `${lead} — ${BYPASS_REJECTIONS[verdict.bypass.reason] ?? verdict.bypass.reason}`,
1596
1619
  ""
1597
1620
  );
1598
1621
  }
@@ -2829,6 +2852,94 @@ export async function applyIssuePlan(api, plan, wait) {
2829
2852
  return Object.freeze(results);
2830
2853
  }
2831
2854
 
2855
+ /**
2856
+ * Reads the pull request's CURRENT labels, body and author, LIVE from the API.
2857
+ *
2858
+ * THIS IS THE BYPASS'S ONLY SOURCE OF TRUTH, and the reason it exists is a
2859
+ * measured defect. The gate used to read both halves of the bypass request from
2860
+ * `github.event.pull_request` — `toJSON(...labels.*.name)` and `...body`. That
2861
+ * object is the event payload CAPTURED WHEN THE RUN WAS TRIGGERED. It is a
2862
+ * snapshot, not a live read, and a re-run REPLAYS the original payload. So the
2863
+ * gate's own printed remedy — "add the trailer, then apply the label" — could
2864
+ * not work: applying a label fires no run under the default activity types, and
2865
+ * re-running (the obvious next move, and the one the failure message invites)
2866
+ * replays a payload from before the label existed. Measured on two consumer
2867
+ * repositories: the label sat on the pull request the whole time while the job
2868
+ * logged `NIGHTLY_PR_LABELS: []`. The only thing that worked was an empty
2869
+ * commit, to manufacture a `synchronize` whose payload happened to carry the
2870
+ * label — a workaround nobody should have to discover.
2871
+ *
2872
+ * A live read is immune to both halves of that: it is correct on a re-run, and
2873
+ * correct whether or not the caller subscribed to the `labeled` activity type.
2874
+ *
2875
+ * It also fixes the mirror-image hole, which matters more: a payload that still
2876
+ * carries a label somebody has since REMOVED would waive a gate whose waiver was
2877
+ * withdrawn. Reading live is the only way "the label is gone" can reach the
2878
+ * gate at all.
2879
+ *
2880
+ * FAILS CLOSED, and never falls back to the payload. `null` means the pull
2881
+ * request could not be read — 404, an unreadable API, a body with no `labels`
2882
+ * array — and the caller turns that into a REJECTED bypass, never into a
2883
+ * bypass. Falling back to the payload here would reintroduce exactly the
2884
+ * stale-label waiver above, on the one path where nobody is watching. Note the
2885
+ * distinction the caller depends on: `null` is "unreadable", while a readable
2886
+ * pull request carrying no labels is `labels: []` — both stay gated, for
2887
+ * reasons the report states differently.
2888
+ *
2889
+ * @param {object} api - API coordinates
2890
+ * @param {number} prNumber - Pull request number
2891
+ * @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
2892
+ * @returns {Promise<{labels: ReadonlyArray<string>, body: string, author: string|null}|null>} Live PR state, or `null` when unreadable
2893
+ */
2894
+ export async function fetchPullRequestState(api, prNumber, wait) {
2895
+ const result = await apiGet(
2896
+ api,
2897
+ `/repos/${api.repo}/pulls/${prNumber}`,
2898
+ wait
2899
+ ).catch(() => null);
2900
+ const pr = result?.body;
2901
+ // A missing `labels` array is UNREADABLE, not "no labels". The two must not
2902
+ // collapse: one is a broken read and the other is a fact about the PR.
2903
+ if (!pr || !Array.isArray(pr.labels)) return null;
2904
+ const labels = pr.labels
2905
+ .map(entry => (typeof entry === "string" ? entry : entry?.name))
2906
+ .filter(name => typeof name === "string" && name.length > 0);
2907
+ return Object.freeze({
2908
+ labels: Object.freeze(labels),
2909
+ body: typeof pr.body === "string" ? pr.body : "",
2910
+ author: typeof pr.user?.login === "string" ? pr.user.login : null,
2911
+ });
2912
+ }
2913
+
2914
+ /**
2915
+ * The emitted decision for "the pull request could not be read".
2916
+ *
2917
+ * Same keys as every `evaluateBypass` outcome, so `audit_json` has one shape
2918
+ * whatever happened — §6's emitted record is asserted key for key.
2919
+ *
2920
+ * @param {{prNumber: number|null, label: string|null, prAuthor: string|null}} subject - What was being evaluated
2921
+ * @returns {{valid: boolean, reason: string}} A rejected bypass
2922
+ */
2923
+ export function unreadablePullRequestBypass({
2924
+ prNumber = null,
2925
+ label = null,
2926
+ prAuthor = null,
2927
+ }) {
2928
+ return Object.freeze({
2929
+ label,
2930
+ prAuthor,
2931
+ prNumber,
2932
+ actorPermission: null,
2933
+ valid: false,
2934
+ reason: "pr_state_unreadable",
2935
+ actor: null,
2936
+ appliedAt: null,
2937
+ expiresAt: null,
2938
+ ticket: null,
2939
+ detail: null,
2940
+ });
2941
+ }
2942
+
2832
2943
  /**
2833
2944
  * Reads who most recently applied the bypass label, from the PR's issue events.
2834
2945
  *
@@ -2992,18 +3103,26 @@ export function resolveSettings(env) {
2992
3103
  // An ADDITIONAL project rule, never a replacement — see `evaluateBypass`.
2993
3104
  extraBypassReasonPattern: env.NIGHTLY_BYPASS_REASON_PATTERN || "",
2994
3105
  clamped,
3106
+ // The ONLY thing taken from the event payload is the pull request NUMBER,
3107
+ // and only because a PR's number is immutable — it is an address, not a
3108
+ // fact about the PR. Everything the bypass decides on (labels, body, and
3109
+ // the author carried onto the audit) is read LIVE by
3110
+ // `fetchPullRequestState`, because `github.event` is a snapshot from
3111
+ // trigger time that a re-run replays verbatim.
3112
+ //
3113
+ // `NIGHTLY_PR_LABELS` and `NIGHTLY_PR_BODY` are DELIBERATELY NOT READ from
3114
+ // here as of contract 1.6.0. The reusable workflow still sets them, on
3115
+ // purpose: a consumer that has taken the newer workflow ref but not yet run
3116
+ // `lisa apply` is still running a pre-1.6.0 guard, and that guard needs
3117
+ // them. Reading them here would defeat the fix — the payload is exactly
3118
+ // what was wrong.
3119
+ //
3120
+ // `payloadAuthor` is a fallback for the audit record only. It never gates
3121
+ // anything (see `evaluateBypass`: `prAuthor` is recorded, never used to
3122
+ // reject), so a stale value cannot change a verdict.
2995
3123
  pr: {
2996
3124
  number: Number(env.NIGHTLY_PR_NUMBER) || null,
2997
- author: env.NIGHTLY_PR_AUTHOR || null,
2998
- body: env.NIGHTLY_PR_BODY || "",
2999
- labels: (() => {
3000
- try {
3001
- const parsed = JSON.parse(env.NIGHTLY_PR_LABELS || "[]");
3002
- return Array.isArray(parsed) ? parsed.map(String) : [];
3003
- } catch {
3004
- return [];
3005
- }
3006
- })(),
3125
+ payloadAuthor: env.NIGHTLY_PR_AUTHOR || null,
3007
3126
  },
3008
3127
  };
3009
3128
  }
@@ -3044,28 +3163,49 @@ export async function runGate(env, wait) {
3044
3163
  return grace.firstSeen === null ? finding : { ...finding, grace };
3045
3164
  });
3046
3165
 
3166
+ // The bypass reads the pull request LIVE and never consults `github.event`.
3167
+ // Three outcomes, and the middle one is the vacuity guard: unreadable is a
3168
+ // REJECTED bypass, never an absent one and never a granted one.
3047
3169
  let bypass = null;
3048
- if (settings.pr.number && settings.pr.labels.includes(settings.bypassLabel)) {
3049
- const labelEvent = await fetchLabelEvent(
3170
+ if (settings.pr.number) {
3171
+ const live = await fetchPullRequestState(
3050
3172
  settings.api,
3051
3173
  settings.pr.number,
3052
- settings.bypassLabel,
3053
3174
  wait
3054
3175
  );
3055
- const actorPermission = labelEvent
3056
- ? await fetchActorPermission(settings.api, labelEvent.actor, wait)
3057
- : null;
3058
- bypass = evaluateBypass({
3059
- labelEvent,
3060
- prAuthor: settings.pr.author,
3061
- prBody: settings.pr.body,
3062
- actorPermission,
3176
+ const subject = {
3063
3177
  prNumber: settings.pr.number,
3064
3178
  label: settings.bypassLabel,
3065
- maxHours: settings.bypassMaxHours,
3066
- extraReasonPattern: settings.extraBypassReasonPattern,
3067
- now,
3068
- });
3179
+ prAuthor: settings.pr.payloadAuthor,
3180
+ };
3181
+ if (live === null) {
3182
+ bypass = unreadablePullRequestBypass(subject);
3183
+ } else if (live.labels.includes(settings.bypassLabel)) {
3184
+ const labelEvent = await fetchLabelEvent(
3185
+ settings.api,
3186
+ settings.pr.number,
3187
+ settings.bypassLabel,
3188
+ wait
3189
+ );
3190
+ const actorPermission = labelEvent
3191
+ ? await fetchActorPermission(settings.api, labelEvent.actor, wait)
3192
+ : null;
3193
+ bypass = evaluateBypass({
3194
+ labelEvent,
3195
+ prAuthor: live.author ?? settings.pr.payloadAuthor,
3196
+ prBody: live.body,
3197
+ actorPermission,
3198
+ prNumber: settings.pr.number,
3199
+ label: settings.bypassLabel,
3200
+ maxHours: settings.bypassMaxHours,
3201
+ extraReasonPattern: settings.extraBypassReasonPattern,
3202
+ now,
3203
+ });
3204
+ }
3205
+ // The remaining case — read successfully, label not present — leaves
3206
+ // `bypass` null. Nobody asked for a waiver, so there is nothing to report,
3207
+ // and a label REMOVED since the run was triggered lands here rather than
3208
+ // waiving anything.
3069
3209
  }
3070
3210
 
3071
3211
  return {