@codyswann/lisa 4.54.13 → 4.55.0

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 (578) hide show
  1. package/all/copy-contents/.gitattributes +8 -0
  2. package/all/copy-overwrite/scripts/check-orphaned-branches.mjs +78 -9
  3. package/all/copy-overwrite/scripts/check-third-party-action-pins.mjs +17 -1
  4. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +194 -7
  5. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +31 -0
  6. package/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +81 -2
  7. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +293 -8
  8. package/all/copy-overwrite/scripts/lisa-hooks/block-instruction-file-edits.sh +21 -0
  9. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +78 -15
  10. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +21 -0
  11. package/all/copy-overwrite/scripts/lisa-hooks/block-shell-json-parsing.sh +21 -0
  12. package/all/copy-overwrite/scripts/lisa-hooks/guard-dedupe.bash +340 -0
  13. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +29 -1
  14. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +183 -1
  15. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.sh +21 -0
  16. package/all/copy-overwrite/scripts/lisa-postinstall.mjs +21 -1
  17. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +100 -5
  18. package/all/copy-overwrite/scripts/lisa-work-item.mjs +1085 -80
  19. package/all/create-only/.github/workflows/continuous-gates.yml +7 -0
  20. package/cdk/create-only/.github/workflows/ci.yml +7 -0
  21. package/cdk/create-only/.github/workflows/deploy.yml +21 -0
  22. package/dist/cli/apply.d.ts.map +1 -1
  23. package/dist/cli/apply.js +9 -1
  24. package/dist/cli/apply.js.map +1 -1
  25. package/dist/cli/doctor-apply-deletions.d.ts +61 -0
  26. package/dist/cli/doctor-apply-deletions.d.ts.map +1 -0
  27. package/dist/cli/doctor-apply-deletions.js +137 -0
  28. package/dist/cli/doctor-apply-deletions.js.map +1 -0
  29. package/dist/cli/doctor-cdk-preset-adoption.d.ts +15 -0
  30. package/dist/cli/doctor-cdk-preset-adoption.d.ts.map +1 -0
  31. package/dist/cli/doctor-cdk-preset-adoption.js +316 -0
  32. package/dist/cli/doctor-cdk-preset-adoption.js.map +1 -0
  33. package/dist/cli/doctor-config-shadowing.d.ts +22 -0
  34. package/dist/cli/doctor-config-shadowing.d.ts.map +1 -0
  35. package/dist/cli/doctor-config-shadowing.js +123 -0
  36. package/dist/cli/doctor-config-shadowing.js.map +1 -0
  37. package/dist/cli/doctor-rails-deploy-intent.d.ts +15 -0
  38. package/dist/cli/doctor-rails-deploy-intent.d.ts.map +1 -0
  39. package/dist/cli/doctor-rails-deploy-intent.js +197 -0
  40. package/dist/cli/doctor-rails-deploy-intent.js.map +1 -0
  41. package/dist/cli/doctor-seeded-artifacts.d.ts +12 -0
  42. package/dist/cli/doctor-seeded-artifacts.d.ts.map +1 -0
  43. package/dist/cli/doctor-seeded-artifacts.js +54 -0
  44. package/dist/cli/doctor-seeded-artifacts.js.map +1 -0
  45. package/dist/cli/doctor-stale-banner-scan.d.ts +44 -0
  46. package/dist/cli/doctor-stale-banner-scan.d.ts.map +1 -0
  47. package/dist/cli/doctor-stale-banner-scan.js +152 -0
  48. package/dist/cli/doctor-stale-banner-scan.js.map +1 -0
  49. package/dist/cli/doctor-stale-managed-banner.d.ts +19 -0
  50. package/dist/cli/doctor-stale-managed-banner.d.ts.map +1 -0
  51. package/dist/cli/doctor-stale-managed-banner.js +198 -0
  52. package/dist/cli/doctor-stale-managed-banner.js.map +1 -0
  53. package/dist/cli/doctor.d.ts.map +1 -1
  54. package/dist/cli/doctor.js +16 -0
  55. package/dist/cli/doctor.js.map +1 -1
  56. package/dist/cli/ui-detected-stacks.d.ts.map +1 -1
  57. package/dist/cli/ui-detected-stacks.js +3 -2
  58. package/dist/cli/ui-detected-stacks.js.map +1 -1
  59. package/dist/cli/update-check.d.ts.map +1 -1
  60. package/dist/cli/update-check.js +14 -0
  61. package/dist/cli/update-check.js.map +1 -1
  62. package/dist/cli/worktree-liveness.d.ts +5 -0
  63. package/dist/cli/worktree-liveness.d.ts.map +1 -1
  64. package/dist/cli/worktree-liveness.js +7 -6
  65. package/dist/cli/worktree-liveness.js.map +1 -1
  66. package/dist/cli/worktree-ownership.d.ts +13 -0
  67. package/dist/cli/worktree-ownership.d.ts.map +1 -1
  68. package/dist/cli/worktree-ownership.js +14 -0
  69. package/dist/cli/worktree-ownership.js.map +1 -1
  70. package/dist/configs/eslint/base.d.ts +34 -0
  71. package/dist/configs/eslint/base.d.ts.map +1 -1
  72. package/dist/configs/eslint/base.js +34 -0
  73. package/dist/configs/eslint/base.js.map +1 -1
  74. package/dist/core/apply-receipt.d.ts +65 -0
  75. package/dist/core/apply-receipt.d.ts.map +1 -1
  76. package/dist/core/apply-receipt.js +62 -2
  77. package/dist/core/apply-receipt.js.map +1 -1
  78. package/dist/core/cdk-preset-adoption.d.ts +136 -0
  79. package/dist/core/cdk-preset-adoption.d.ts.map +1 -0
  80. package/dist/core/cdk-preset-adoption.js +135 -0
  81. package/dist/core/cdk-preset-adoption.js.map +1 -0
  82. package/dist/core/config-shadowing.d.ts +36 -0
  83. package/dist/core/config-shadowing.d.ts.map +1 -1
  84. package/dist/core/config-shadowing.js +38 -0
  85. package/dist/core/config-shadowing.js.map +1 -1
  86. package/dist/core/config.d.ts +11 -0
  87. package/dist/core/config.d.ts.map +1 -1
  88. package/dist/core/config.js.map +1 -1
  89. package/dist/core/learnings-merge-driver.d.ts +10 -0
  90. package/dist/core/learnings-merge-driver.d.ts.map +1 -1
  91. package/dist/core/learnings-merge-driver.js +18 -0
  92. package/dist/core/learnings-merge-driver.js.map +1 -1
  93. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  94. package/dist/core/lisa-owned-hash-ledger.js +136 -0
  95. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  96. package/dist/core/lisa.d.ts.map +1 -1
  97. package/dist/core/lisa.js +9 -1
  98. package/dist/core/lisa.js.map +1 -1
  99. package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts +5 -5
  100. package/dist/core/nightly-e2e-guard-behavior-certificate.js +7 -7
  101. package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  102. package/dist/core/ownership-header.d.ts +74 -0
  103. package/dist/core/ownership-header.d.ts.map +1 -0
  104. package/dist/core/ownership-header.js +76 -0
  105. package/dist/core/ownership-header.js.map +1 -0
  106. package/dist/core/rails-deploy-production-intent.d.ts +36 -0
  107. package/dist/core/rails-deploy-production-intent.d.ts.map +1 -0
  108. package/dist/core/rails-deploy-production-intent.js +79 -0
  109. package/dist/core/rails-deploy-production-intent.js.map +1 -0
  110. package/dist/core/stale-managed-banner.d.ts +78 -0
  111. package/dist/core/stale-managed-banner.d.ts.map +1 -0
  112. package/dist/core/stale-managed-banner.js +118 -0
  113. package/dist/core/stale-managed-banner.js.map +1 -0
  114. package/dist/core/two-channel-delivery-scan.d.ts +33 -1
  115. package/dist/core/two-channel-delivery-scan.d.ts.map +1 -1
  116. package/dist/core/two-channel-delivery-scan.js +133 -6
  117. package/dist/core/two-channel-delivery-scan.js.map +1 -1
  118. package/dist/core/two-channel-delivery.d.ts +101 -4
  119. package/dist/core/two-channel-delivery.d.ts.map +1 -1
  120. package/dist/core/two-channel-delivery.js +121 -13
  121. package/dist/core/two-channel-delivery.js.map +1 -1
  122. package/dist/core/two-channel-staleness.d.ts +137 -0
  123. package/dist/core/two-channel-staleness.d.ts.map +1 -0
  124. package/dist/core/two-channel-staleness.js +268 -0
  125. package/dist/core/two-channel-staleness.js.map +1 -0
  126. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  127. package/dist/core/upstream-evidence-manifest.js +265 -121
  128. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  129. package/dist/core/workflow-deletion-ownership.d.ts +6 -24
  130. package/dist/core/workflow-deletion-ownership.d.ts.map +1 -1
  131. package/dist/core/workflow-deletion-ownership.js +22 -42
  132. package/dist/core/workflow-deletion-ownership.js.map +1 -1
  133. package/dist/detection/detectors/cdk.d.ts.map +1 -1
  134. package/dist/detection/detectors/cdk.js +2 -1
  135. package/dist/detection/detectors/cdk.js.map +1 -1
  136. package/dist/health/template-inspection.d.ts.map +1 -1
  137. package/dist/health/template-inspection.js +2 -1
  138. package/dist/health/template-inspection.js.map +1 -1
  139. package/dist/migrations/ensure-lisa-postinstall.d.ts +16 -0
  140. package/dist/migrations/ensure-lisa-postinstall.d.ts.map +1 -1
  141. package/dist/migrations/ensure-lisa-postinstall.js +16 -0
  142. package/dist/migrations/ensure-lisa-postinstall.js.map +1 -1
  143. package/dist/migrations/ensure-pinned-reusable-workflow-refs.d.ts +2 -2
  144. package/dist/migrations/ensure-pinned-reusable-workflow-refs.d.ts.map +1 -1
  145. package/dist/migrations/ensure-pinned-reusable-workflow-refs.js +98 -34
  146. package/dist/migrations/ensure-pinned-reusable-workflow-refs.js.map +1 -1
  147. package/dist/migrations/ensure-playwright-dedicated-caller.d.ts.map +1 -1
  148. package/dist/migrations/ensure-playwright-dedicated-caller.js +1 -0
  149. package/dist/migrations/ensure-playwright-dedicated-caller.js.map +1 -1
  150. package/dist/opencode/hooks-installer.d.ts.map +1 -1
  151. package/dist/opencode/hooks-installer.js +1 -0
  152. package/dist/opencode/hooks-installer.js.map +1 -1
  153. package/dist/opencode/plugin-templates/block-managed-file-edits.sh +78 -15
  154. package/dist/opencode/plugin-templates/block-no-verify.sh +21 -0
  155. package/dist/opencode/plugin-templates/guard-dedupe.bash +337 -0
  156. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +242 -55
  157. package/dist/opencode/plugin-templates/parity-safety-net.sh +29 -1
  158. package/dist/strategies/package-lisa.d.ts.map +1 -1
  159. package/dist/strategies/package-lisa.js +2 -1
  160. package/dist/strategies/package-lisa.js.map +1 -1
  161. package/dist/utils/path-utils.d.ts +29 -0
  162. package/dist/utils/path-utils.d.ts.map +1 -1
  163. package/dist/utils/path-utils.js +41 -0
  164. package/dist/utils/path-utils.js.map +1 -1
  165. package/expo/create-only/.github/workflows/ci.yml +21 -0
  166. package/expo/create-only/.github/workflows/deploy.yml +28 -0
  167. package/expo/create-only/.github/workflows/maestro-e2e.yml +7 -0
  168. package/expo/create-only/.github/workflows/nightly-e2e-bypass-reaper.yml +124 -13
  169. package/expo/create-only/.github/workflows/nightly-e2e-health.yml +7 -0
  170. package/expo/create-only/.github/workflows/nightly-e2e-report.yml +7 -0
  171. package/expo/create-only/.github/workflows/nightly-e2e-tracking.yml +8 -0
  172. package/expo/create-only/.github/workflows/playwright-e2e.yml +7 -0
  173. package/harper-fabric/copy-overwrite/.github/workflows/ci.yml +7 -0
  174. package/harper-fabric/create-only/.github/workflows/deploy.yml +14 -0
  175. package/nestjs/create-only/.github/workflows/ci.yml +14 -0
  176. package/nestjs/create-only/.github/workflows/deploy.yml +28 -0
  177. package/package.json +10 -5
  178. package/phaser/copy-overwrite/.github/workflows/ci.yml +7 -0
  179. package/plugins/lisa/.claude-plugin/plugin.json +28 -1
  180. package/plugins/lisa/.codex-plugin/hooks.json +27 -0
  181. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  182. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
  183. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +27 -4
  184. package/plugins/lisa/.codex-plugin/skills/lisa-github-prd-intake/SKILL.md +1 -1
  185. package/plugins/lisa/.codex-plugin/skills/lisa-github-validate-issue/SKILL.md +22 -0
  186. package/plugins/lisa/.codex-plugin/skills/lisa-github-verify/SKILL.md +14 -0
  187. package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +51 -0
  188. package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +1 -1
  189. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +27 -4
  190. package/plugins/lisa/.codex-plugin/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  191. package/plugins/lisa/.codex-plugin/skills/lisa-jira-verify/SKILL.md +14 -0
  192. package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +51 -0
  193. package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +55 -2
  194. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +27 -4
  195. package/plugins/lisa/.codex-plugin/skills/lisa-linear-prd-intake/SKILL.md +47 -30
  196. package/plugins/lisa/.codex-plugin/skills/lisa-linear-to-tracker/SKILL.md +3 -3
  197. package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  198. package/plugins/lisa/.codex-plugin/skills/lisa-linear-verify/SKILL.md +14 -0
  199. package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +54 -3
  200. package/plugins/lisa/.codex-plugin/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  201. package/plugins/lisa/.codex-plugin/skills/lisa-qa-clear/SKILL.md +19 -7
  202. package/plugins/lisa/.codex-plugin/skills/lisa-qa-fail/SKILL.md +28 -3
  203. package/plugins/lisa/.codex-plugin/skills/lisa-qa-queue/SKILL.md +28 -3
  204. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +94 -24
  205. package/plugins/lisa/.codex-plugin/skills/lisa-rework-triage/SKILL.md +32 -2
  206. package/plugins/lisa/.codex-plugin/skills/lisa-setup-linear/SKILL.md +4 -4
  207. package/plugins/lisa/.codex-plugin/skills/lisa-track/SKILL.md +1 -1
  208. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-build-intake/SKILL.md +9 -0
  209. package/plugins/lisa/agents/linear-prd-intake.md +8 -8
  210. package/plugins/lisa/hooks/block-blind-automerge.sh +81 -2
  211. package/plugins/lisa/hooks/block-direct-issue-create.agy.sh +64 -16
  212. package/plugins/lisa/hooks/block-direct-issue-create.sh +293 -8
  213. package/plugins/lisa/hooks/block-instruction-file-edits.sh +21 -0
  214. package/plugins/lisa/hooks/block-managed-file-edits.sh +78 -15
  215. package/plugins/lisa/hooks/block-no-verify.sh +21 -0
  216. package/plugins/lisa/hooks/block-shell-json-parsing.sh +21 -0
  217. package/plugins/lisa/hooks/failure-signature-index.mjs +240 -5
  218. package/plugins/lisa/hooks/guard-dedupe.bash +337 -0
  219. package/plugins/lisa/hooks/inject-rules.sh +13 -5
  220. package/plugins/lisa/hooks/operational-hazards.mjs +736 -0
  221. package/plugins/lisa/hooks/operational-hazards.sh +28 -0
  222. package/plugins/lisa/hooks/parity-safety-net.sh +29 -1
  223. package/plugins/lisa/hooks/threshold-ratchet-compare.mjs +102 -21
  224. package/plugins/lisa/hooks/threshold-ratchet-families.mjs +130 -2
  225. package/plugins/lisa/hooks/threshold-ratchet.mjs +63 -13
  226. package/plugins/lisa/hooks/worktree-binding-guard.mjs +183 -1
  227. package/plugins/lisa/hooks/worktree-binding-guard.sh +21 -0
  228. package/plugins/lisa/rules/eager/00-rule-index.md +1 -0
  229. package/plugins/lisa/rules/eager/operational-hazards.md +50 -0
  230. package/plugins/lisa/rules/eager/tracked-work.md +1 -1
  231. package/plugins/lisa/rules/reference/config-resolution.md +1 -1
  232. package/plugins/lisa/rules/reference/leaf-only-lifecycle.md +1 -1
  233. package/plugins/lisa/rules/reference/operational-hazards.md +115 -0
  234. package/plugins/lisa/rules/reference/work-item-trailer-definition.md +129 -0
  235. package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +407 -26
  236. package/plugins/lisa/scripts/qa-signal-lifecycle.mjs +507 -0
  237. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
  238. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +27 -4
  239. package/plugins/lisa/skills/lisa-github-prd-intake/SKILL.md +1 -1
  240. package/plugins/lisa/skills/lisa-github-validate-issue/SKILL.md +22 -0
  241. package/plugins/lisa/skills/lisa-github-verify/SKILL.md +14 -0
  242. package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +51 -0
  243. package/plugins/lisa/skills/lisa-intake/SKILL.md +1 -1
  244. package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +27 -4
  245. package/plugins/lisa/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  246. package/plugins/lisa/skills/lisa-jira-verify/SKILL.md +14 -0
  247. package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +51 -0
  248. package/plugins/lisa/skills/lisa-linear-access/SKILL.md +55 -2
  249. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +27 -4
  250. package/plugins/lisa/skills/lisa-linear-prd-intake/SKILL.md +48 -31
  251. package/plugins/lisa/skills/lisa-linear-to-tracker/SKILL.md +3 -3
  252. package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  253. package/plugins/lisa/skills/lisa-linear-verify/SKILL.md +14 -0
  254. package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +54 -3
  255. package/plugins/lisa/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  256. package/plugins/lisa/skills/lisa-qa-clear/SKILL.md +19 -7
  257. package/plugins/lisa/skills/lisa-qa-fail/SKILL.md +28 -3
  258. package/plugins/lisa/skills/lisa-qa-queue/SKILL.md +28 -3
  259. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +94 -24
  260. package/plugins/lisa/skills/lisa-rework-triage/SKILL.md +32 -2
  261. package/plugins/lisa/skills/lisa-setup-linear/SKILL.md +5 -5
  262. package/plugins/lisa/skills/lisa-track/SKILL.md +1 -1
  263. package/plugins/lisa/skills/lisa-tracker-build-intake/SKILL.md +9 -0
  264. package/plugins/lisa-agy/agents/linear-prd-intake.md +8 -8
  265. package/plugins/lisa-agy/hooks/block-blind-automerge.sh +81 -2
  266. package/plugins/lisa-agy/hooks/block-direct-issue-create.agy.sh +64 -16
  267. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +293 -8
  268. package/plugins/lisa-agy/hooks/block-instruction-file-edits.sh +21 -0
  269. package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +78 -15
  270. package/plugins/lisa-agy/hooks/block-shell-json-parsing.sh +21 -0
  271. package/plugins/lisa-agy/hooks/guard-dedupe.bash +337 -0
  272. package/plugins/lisa-agy/hooks/parity-safety-net.sh +29 -1
  273. package/plugins/lisa-agy/hooks.json +1 -1
  274. package/plugins/lisa-agy/plugin.json +1 -1
  275. package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +407 -26
  276. package/plugins/lisa-agy/scripts/qa-signal-lifecycle.mjs +507 -0
  277. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
  278. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +27 -4
  279. package/plugins/lisa-agy/skills/lisa-github-prd-intake/SKILL.md +1 -1
  280. package/plugins/lisa-agy/skills/lisa-github-validate-issue/SKILL.md +22 -0
  281. package/plugins/lisa-agy/skills/lisa-github-verify/SKILL.md +14 -0
  282. package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +51 -0
  283. package/plugins/lisa-agy/skills/lisa-intake/SKILL.md +1 -1
  284. package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +27 -4
  285. package/plugins/lisa-agy/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  286. package/plugins/lisa-agy/skills/lisa-jira-verify/SKILL.md +14 -0
  287. package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +51 -0
  288. package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +55 -2
  289. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +27 -4
  290. package/plugins/lisa-agy/skills/lisa-linear-prd-intake/SKILL.md +48 -31
  291. package/plugins/lisa-agy/skills/lisa-linear-to-tracker/SKILL.md +3 -3
  292. package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  293. package/plugins/lisa-agy/skills/lisa-linear-verify/SKILL.md +14 -0
  294. package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +54 -3
  295. package/plugins/lisa-agy/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  296. package/plugins/lisa-agy/skills/lisa-qa-clear/SKILL.md +19 -7
  297. package/plugins/lisa-agy/skills/lisa-qa-fail/SKILL.md +28 -3
  298. package/plugins/lisa-agy/skills/lisa-qa-queue/SKILL.md +28 -3
  299. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +94 -24
  300. package/plugins/lisa-agy/skills/lisa-rework-triage/SKILL.md +32 -2
  301. package/plugins/lisa-agy/skills/lisa-setup-linear/SKILL.md +5 -5
  302. package/plugins/lisa-agy/skills/lisa-track/SKILL.md +1 -1
  303. package/plugins/lisa-agy/skills/lisa-tracker-build-intake/SKILL.md +9 -0
  304. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  305. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  306. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  307. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  308. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  309. package/plugins/lisa-copilot/.claude-plugin/plugin.json +19 -1
  310. package/plugins/lisa-copilot/agents/linear-prd-intake.agent.md +8 -8
  311. package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +81 -2
  312. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +293 -8
  313. package/plugins/lisa-copilot/hooks/block-instruction-file-edits.sh +21 -0
  314. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +78 -15
  315. package/plugins/lisa-copilot/hooks/block-no-verify.sh +21 -0
  316. package/plugins/lisa-copilot/hooks/block-shell-json-parsing.sh +21 -0
  317. package/plugins/lisa-copilot/hooks/failure-signature-index.mjs +240 -5
  318. package/plugins/lisa-copilot/hooks/guard-dedupe.bash +337 -0
  319. package/plugins/lisa-copilot/hooks/inject-rules.sh +13 -5
  320. package/plugins/lisa-copilot/hooks/operational-hazards.mjs +736 -0
  321. package/plugins/lisa-copilot/hooks/operational-hazards.sh +28 -0
  322. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +29 -1
  323. package/plugins/lisa-copilot/hooks/threshold-ratchet-compare.mjs +102 -21
  324. package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +130 -2
  325. package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +63 -13
  326. package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +183 -1
  327. package/plugins/lisa-copilot/hooks/worktree-binding-guard.sh +21 -0
  328. package/plugins/lisa-copilot/rules/eager/00-rule-index.md +1 -0
  329. package/plugins/lisa-copilot/rules/eager/operational-hazards.md +50 -0
  330. package/plugins/lisa-copilot/rules/eager/tracked-work.md +1 -1
  331. package/plugins/lisa-copilot/rules/reference/config-resolution.md +1 -1
  332. package/plugins/lisa-copilot/rules/reference/leaf-only-lifecycle.md +1 -1
  333. package/plugins/lisa-copilot/rules/reference/operational-hazards.md +115 -0
  334. package/plugins/lisa-copilot/rules/reference/work-item-trailer-definition.md +129 -0
  335. package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +407 -26
  336. package/plugins/lisa-copilot/scripts/qa-signal-lifecycle.mjs +507 -0
  337. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
  338. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +27 -4
  339. package/plugins/lisa-copilot/skills/lisa-github-prd-intake/SKILL.md +1 -1
  340. package/plugins/lisa-copilot/skills/lisa-github-validate-issue/SKILL.md +22 -0
  341. package/plugins/lisa-copilot/skills/lisa-github-verify/SKILL.md +14 -0
  342. package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +51 -0
  343. package/plugins/lisa-copilot/skills/lisa-intake/SKILL.md +1 -1
  344. package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +27 -4
  345. package/plugins/lisa-copilot/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  346. package/plugins/lisa-copilot/skills/lisa-jira-verify/SKILL.md +14 -0
  347. package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +51 -0
  348. package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +55 -2
  349. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +27 -4
  350. package/plugins/lisa-copilot/skills/lisa-linear-prd-intake/SKILL.md +48 -31
  351. package/plugins/lisa-copilot/skills/lisa-linear-to-tracker/SKILL.md +3 -3
  352. package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  353. package/plugins/lisa-copilot/skills/lisa-linear-verify/SKILL.md +14 -0
  354. package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +54 -3
  355. package/plugins/lisa-copilot/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  356. package/plugins/lisa-copilot/skills/lisa-qa-clear/SKILL.md +19 -7
  357. package/plugins/lisa-copilot/skills/lisa-qa-fail/SKILL.md +28 -3
  358. package/plugins/lisa-copilot/skills/lisa-qa-queue/SKILL.md +28 -3
  359. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +94 -24
  360. package/plugins/lisa-copilot/skills/lisa-rework-triage/SKILL.md +32 -2
  361. package/plugins/lisa-copilot/skills/lisa-setup-linear/SKILL.md +5 -5
  362. package/plugins/lisa-copilot/skills/lisa-track/SKILL.md +1 -1
  363. package/plugins/lisa-copilot/skills/lisa-tracker-build-intake/SKILL.md +9 -0
  364. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  365. package/plugins/lisa-cursor/agents/linear-prd-intake.md +8 -8
  366. package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +81 -2
  367. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +293 -8
  368. package/plugins/lisa-cursor/hooks/block-instruction-file-edits.sh +21 -0
  369. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +78 -15
  370. package/plugins/lisa-cursor/hooks/block-no-verify.sh +21 -0
  371. package/plugins/lisa-cursor/hooks/block-shell-json-parsing.sh +21 -0
  372. package/plugins/lisa-cursor/hooks/failure-signature-index.mjs +240 -5
  373. package/plugins/lisa-cursor/hooks/guard-dedupe.bash +337 -0
  374. package/plugins/lisa-cursor/hooks/hooks.json +10 -0
  375. package/plugins/lisa-cursor/hooks/operational-hazards.mjs +736 -0
  376. package/plugins/lisa-cursor/hooks/operational-hazards.sh +28 -0
  377. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +29 -1
  378. package/plugins/lisa-cursor/hooks/threshold-ratchet-compare.mjs +102 -21
  379. package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +130 -2
  380. package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +63 -13
  381. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +183 -1
  382. package/plugins/lisa-cursor/hooks/worktree-binding-guard.sh +21 -0
  383. package/plugins/lisa-cursor/rules/00-rule-index.mdc +1 -0
  384. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -1
  385. package/plugins/lisa-cursor/rules/leaf-only-lifecycle-reference.mdc +1 -1
  386. package/plugins/lisa-cursor/rules/operational-hazards-reference.mdc +120 -0
  387. package/plugins/lisa-cursor/rules/operational-hazards.mdc +55 -0
  388. package/plugins/lisa-cursor/rules/tracked-work.mdc +1 -1
  389. package/plugins/lisa-cursor/rules/work-item-trailer-definition-reference.mdc +134 -0
  390. package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +407 -26
  391. package/plugins/lisa-cursor/scripts/qa-signal-lifecycle.mjs +507 -0
  392. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
  393. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +27 -4
  394. package/plugins/lisa-cursor/skills/lisa-github-prd-intake/SKILL.md +1 -1
  395. package/plugins/lisa-cursor/skills/lisa-github-validate-issue/SKILL.md +22 -0
  396. package/plugins/lisa-cursor/skills/lisa-github-verify/SKILL.md +14 -0
  397. package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +51 -0
  398. package/plugins/lisa-cursor/skills/lisa-intake/SKILL.md +1 -1
  399. package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +27 -4
  400. package/plugins/lisa-cursor/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  401. package/plugins/lisa-cursor/skills/lisa-jira-verify/SKILL.md +14 -0
  402. package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +51 -0
  403. package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +55 -2
  404. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +27 -4
  405. package/plugins/lisa-cursor/skills/lisa-linear-prd-intake/SKILL.md +48 -31
  406. package/plugins/lisa-cursor/skills/lisa-linear-to-tracker/SKILL.md +3 -3
  407. package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  408. package/plugins/lisa-cursor/skills/lisa-linear-verify/SKILL.md +14 -0
  409. package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +54 -3
  410. package/plugins/lisa-cursor/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  411. package/plugins/lisa-cursor/skills/lisa-qa-clear/SKILL.md +19 -7
  412. package/plugins/lisa-cursor/skills/lisa-qa-fail/SKILL.md +28 -3
  413. package/plugins/lisa-cursor/skills/lisa-qa-queue/SKILL.md +28 -3
  414. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +94 -24
  415. package/plugins/lisa-cursor/skills/lisa-rework-triage/SKILL.md +32 -2
  416. package/plugins/lisa-cursor/skills/lisa-setup-linear/SKILL.md +5 -5
  417. package/plugins/lisa-cursor/skills/lisa-track/SKILL.md +1 -1
  418. package/plugins/lisa-cursor/skills/lisa-tracker-build-intake/SKILL.md +9 -0
  419. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  420. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  421. package/plugins/lisa-expo-agy/plugin.json +1 -1
  422. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  423. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  424. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  425. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  426. package/plugins/lisa-harper-fabric/hooks/inject-rules.sh +14 -3
  427. package/plugins/lisa-harper-fabric/rules/eager/harper-fabric.md +31 -0
  428. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  429. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  430. package/plugins/lisa-harper-fabric-copilot/hooks/inject-rules.sh +14 -3
  431. package/plugins/lisa-harper-fabric-copilot/rules/eager/harper-fabric.md +31 -0
  432. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  433. package/plugins/lisa-harper-fabric-cursor/rules/harper-fabric-reference.mdc +57 -0
  434. package/plugins/lisa-harper-fabric-cursor/rules/harper-fabric.mdc +32 -53
  435. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  436. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  437. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  438. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  439. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  440. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  441. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  442. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  443. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  444. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  445. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  446. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  447. package/plugins/lisa-phaser/hooks/inject-rules.sh +14 -3
  448. package/plugins/lisa-phaser/rules/eager/phaser.md +44 -0
  449. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  450. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  451. package/plugins/lisa-phaser-copilot/hooks/inject-rules.sh +14 -3
  452. package/plugins/lisa-phaser-copilot/rules/eager/phaser.md +44 -0
  453. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  454. package/plugins/lisa-phaser-cursor/rules/phaser-reference.mdc +189 -0
  455. package/plugins/lisa-phaser-cursor/rules/phaser.mdc +45 -185
  456. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  457. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  458. package/plugins/lisa-rails/hooks/inject-rules.sh +14 -4
  459. package/plugins/lisa-rails/rules/eager/rails-conventions.md +23 -0
  460. package/plugins/lisa-rails-agy/plugin.json +1 -1
  461. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  462. package/plugins/lisa-rails-copilot/hooks/inject-rules.sh +14 -4
  463. package/plugins/lisa-rails-copilot/rules/eager/rails-conventions.md +23 -0
  464. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  465. package/plugins/lisa-rails-cursor/rules/rails-conventions-reference.mdc +181 -0
  466. package/plugins/lisa-rails-cursor/rules/rails-conventions.mdc +24 -177
  467. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  468. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  469. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  470. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  471. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  472. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  473. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  474. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  475. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  476. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  477. package/plugins/materialized-artifacts.json +1 -0
  478. package/plugins/src/base/.claude-plugin/plugin.json +27 -0
  479. package/plugins/src/base/agents/linear-prd-intake.md +8 -8
  480. package/plugins/src/base/hooks/block-blind-automerge.sh +81 -2
  481. package/plugins/src/base/hooks/block-direct-issue-create.agy.sh +64 -16
  482. package/plugins/src/base/hooks/block-direct-issue-create.sh +293 -8
  483. package/plugins/src/base/hooks/block-instruction-file-edits.sh +21 -0
  484. package/plugins/src/base/hooks/block-managed-file-edits.sh +78 -15
  485. package/plugins/src/base/hooks/block-no-verify.sh +21 -0
  486. package/plugins/src/base/hooks/block-shell-json-parsing.sh +21 -0
  487. package/plugins/src/base/hooks/failure-signature-index.mjs +240 -5
  488. package/plugins/src/base/hooks/guard-dedupe.bash +337 -0
  489. package/plugins/src/base/hooks/inject-rules.sh +13 -5
  490. package/plugins/src/base/hooks/operational-hazards.mjs +736 -0
  491. package/plugins/src/base/hooks/operational-hazards.sh +28 -0
  492. package/plugins/src/base/hooks/parity-safety-net.sh +29 -1
  493. package/plugins/src/base/hooks/threshold-ratchet-compare.mjs +102 -21
  494. package/plugins/src/base/hooks/threshold-ratchet-families.mjs +130 -2
  495. package/plugins/src/base/hooks/threshold-ratchet.mjs +63 -13
  496. package/plugins/src/base/hooks/worktree-binding-guard.mjs +183 -1
  497. package/plugins/src/base/hooks/worktree-binding-guard.sh +21 -0
  498. package/plugins/src/base/rules/eager/00-rule-index.md +1 -0
  499. package/plugins/src/base/rules/eager/operational-hazards.md +50 -0
  500. package/plugins/src/base/rules/eager/tracked-work.md +1 -1
  501. package/plugins/src/base/rules/reference/config-resolution.md +1 -1
  502. package/plugins/src/base/rules/reference/leaf-only-lifecycle.md +1 -1
  503. package/plugins/src/base/rules/reference/operational-hazards.md +115 -0
  504. package/plugins/src/base/rules/reference/work-item-trailer-definition.md +129 -0
  505. package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +407 -26
  506. package/plugins/src/base/scripts/qa-signal-lifecycle.mjs +507 -0
  507. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +130 -19
  508. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +27 -4
  509. package/plugins/src/base/skills/lisa-github-prd-intake/SKILL.md +1 -1
  510. package/plugins/src/base/skills/lisa-github-validate-issue/SKILL.md +22 -0
  511. package/plugins/src/base/skills/lisa-github-verify/SKILL.md +14 -0
  512. package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +51 -0
  513. package/plugins/src/base/skills/lisa-intake/SKILL.md +1 -1
  514. package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +27 -4
  515. package/plugins/src/base/skills/lisa-jira-validate-ticket/SKILL.md +22 -0
  516. package/plugins/src/base/skills/lisa-jira-verify/SKILL.md +14 -0
  517. package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +51 -0
  518. package/plugins/src/base/skills/lisa-linear-access/SKILL.md +55 -2
  519. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +27 -4
  520. package/plugins/src/base/skills/lisa-linear-prd-intake/SKILL.md +48 -31
  521. package/plugins/src/base/skills/lisa-linear-to-tracker/SKILL.md +3 -3
  522. package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +22 -0
  523. package/plugins/src/base/skills/lisa-linear-verify/SKILL.md +14 -0
  524. package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +54 -3
  525. package/plugins/src/base/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  526. package/plugins/src/base/skills/lisa-qa-clear/SKILL.md +19 -7
  527. package/plugins/src/base/skills/lisa-qa-fail/SKILL.md +28 -3
  528. package/plugins/src/base/skills/lisa-qa-queue/SKILL.md +28 -3
  529. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +94 -24
  530. package/plugins/src/base/skills/lisa-rework-triage/SKILL.md +32 -2
  531. package/plugins/src/base/skills/lisa-setup-linear/SKILL.md +5 -5
  532. package/plugins/src/base/skills/lisa-track/SKILL.md +1 -1
  533. package/plugins/src/base/skills/lisa-tracker-build-intake/SKILL.md +9 -0
  534. package/plugins/src/harper-fabric/hooks/inject-rules.sh +14 -3
  535. package/plugins/src/harper-fabric/rules/eager/harper-fabric.md +31 -0
  536. package/plugins/src/phaser/hooks/inject-rules.sh +14 -3
  537. package/plugins/src/phaser/rules/eager/phaser.md +44 -0
  538. package/plugins/src/rails/hooks/inject-rules.sh +14 -4
  539. package/plugins/src/rails/rules/eager/rails-conventions.md +23 -0
  540. package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +63 -13
  541. package/rails/copy-overwrite/scripts/threshold-ratchet-compare.mjs +102 -21
  542. package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +130 -2
  543. package/rails/create-only/.github/workflows/ci.yml +8 -0
  544. package/rails/create-only/.github/workflows/deploy.yml +22 -0
  545. package/scripts/build-plugins.sh +2 -1
  546. package/scripts/check-engine-floor.mjs +361 -0
  547. package/scripts/check-guard-parity-notes.mjs +709 -0
  548. package/scripts/check-template-workflow-refs.mjs +9 -0
  549. package/scripts/check-third-party-action-pins.mjs +17 -1
  550. package/scripts/check-workflow-contract-assertions.mjs +480 -0
  551. package/scripts/copy-opencode-plugin-templates.mjs +1 -0
  552. package/scripts/deployed-guard-advice.mjs +636 -0
  553. package/scripts/generate-agy-plugin-artifacts.mjs +35 -10
  554. package/scripts/generate-export-surface.mjs +105 -19
  555. package/scripts/generate-two-channel-couplings.ts +143 -212
  556. package/scripts/lisa-enforcement-fallback.sh +31 -0
  557. package/scripts/measure-scripts-profile-gap.mts +361 -0
  558. package/scripts/mutation-performance-measure.mjs +21 -5
  559. package/scripts/reconcile-release-tags.mjs +65 -84
  560. package/scripts/two-channel-couplings.json +302 -70
  561. package/scripts/workflow-contract-assertion.sh +47 -0
  562. package/typescript/copy-overwrite/.lintstagedrc.json +1 -0
  563. package/typescript/copy-overwrite/eslint.ignore.config.json +1 -0
  564. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +527 -53
  565. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +301 -13
  566. package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +63 -13
  567. package/typescript/copy-overwrite/scripts/threshold-ratchet-compare.mjs +102 -21
  568. package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +130 -2
  569. package/typescript/create-only/.github/workflows/ci.yml +7 -0
  570. /package/plugins/lisa-harper-fabric/rules/{harper-fabric.md → reference/harper-fabric.md} +0 -0
  571. /package/plugins/lisa-harper-fabric-copilot/rules/{harper-fabric.md → reference/harper-fabric.md} +0 -0
  572. /package/plugins/lisa-phaser/rules/{phaser.md → reference/phaser.md} +0 -0
  573. /package/plugins/lisa-phaser-copilot/rules/{phaser.md → reference/phaser.md} +0 -0
  574. /package/plugins/lisa-rails/rules/{rails-conventions.md → reference/rails-conventions.md} +0 -0
  575. /package/plugins/lisa-rails-copilot/rules/{rails-conventions.md → reference/rails-conventions.md} +0 -0
  576. /package/plugins/src/harper-fabric/rules/{harper-fabric.md → reference/harper-fabric.md} +0 -0
  577. /package/plugins/src/phaser/rules/{phaser.md → reference/phaser.md} +0 -0
  578. /package/plugins/src/rails/rules/{rails-conventions.md → reference/rails-conventions.md} +0 -0
@@ -1529,6 +1529,37 @@ export function resolveSuiteGrace(suite, maxDays, now) {
1529
1529
  // 4. Bypass — maintainers only, reason required, auto-expiring, self-service
1530
1530
  // ---------------------------------------------------------------------------
1531
1531
 
1532
+ /**
1533
+ * Reads the `Nightly-E2E-Bypass: <TICKET> <reason>` trailer out of a body.
1534
+ *
1535
+ * Split out because the trailer is read by three consumers that MUST agree:
1536
+ * `evaluateBypass` (does this waiver qualify?), the merge-time re-derivation
1537
+ * (does it still qualify NOW?), and the durable record (what did it say?). The
1538
+ * third one is why this returns the trailer independently of validity — an
1539
+ * EXPIRED waiver still has a ticket and a reason, and a record that omitted
1540
+ * them because the waiver had lapsed would answer "what shipped on a waiver,
1541
+ * and why" with silence for exactly the merges most worth reading about.
1542
+ *
1543
+ * The body is read with fenced code blanked (`stripMarkdownCode`): a waiver is
1544
+ * prose that ASSERTS something, and a code sample never is. The strip lives
1545
+ * HERE rather than at one call site precisely because there are three of them
1546
+ * — a pull request that merely DOCUMENTS the trailer format would otherwise
1547
+ * waive its own gate through whichever consumer had not been fixed.
1548
+ *
1549
+ * @param {string|null|undefined} prBody - The pull request body
1550
+ * @returns {{ticket: string, reason: string|null}|null} The trailer, or `null` when absent
1551
+ */
1552
+ export function parseWaiverTrailer(prBody) {
1553
+ const found = new RegExp(REQUIRED_BYPASS_REASON_PATTERN, "m").exec(
1554
+ stripMarkdownCode(prBody ?? "")
1555
+ );
1556
+ if (!found) return null;
1557
+ return Object.freeze({
1558
+ ticket: found.groups?.ticket ?? null,
1559
+ reason: (found.groups?.reason ?? "").trim() || null,
1560
+ });
1561
+ }
1562
+
1532
1563
  /**
1533
1564
  * Decides whether a bypass request is valid, as a pure function of the facts
1534
1565
  * the caller gathered.
@@ -1614,15 +1645,15 @@ export function evaluateBypass(request) {
1614
1645
  }
1615
1646
 
1616
1647
  // The built-in rule ALWAYS applies. It is checked first and on its own, so no
1617
- // caller-supplied pattern can stand in for it.
1618
- //
1619
- // Matched against the body with fenced code blanked: a waiver is prose that
1620
- // ASSERTS something, and a code sample never is. See `stripMarkdownCode`.
1648
+ // caller-supplied pattern can stand in for it. Parsed through the ONE shared
1649
+ // reader (`parseWaiverTrailer`), so the merge-time re-derivation and the
1650
+ // durable record cannot come to read the trailer differently from the gate
1651
+ // and that reader blanks fenced code before matching, because a waiver is
1652
+ // prose that ASSERTS something and a code sample never is. `assertedBody` is
1653
+ // the same view, kept so the optional project pattern below sees it too.
1621
1654
  const assertedBody = stripMarkdownCode(prBody ?? "");
1622
- const found = new RegExp(REQUIRED_BYPASS_REASON_PATTERN, "m").exec(
1623
- assertedBody
1624
- );
1625
- if (!found) {
1655
+ const trailer = parseWaiverTrailer(prBody);
1656
+ if (!trailer) {
1626
1657
  return reject("no_reason_or_ticket", {
1627
1658
  expiresAt: new Date(expiresMs).toISOString(),
1628
1659
  });
@@ -1655,8 +1686,8 @@ export function evaluateBypass(request) {
1655
1686
  actor: labelEvent.actor,
1656
1687
  appliedAt: new Date(appliedMs).toISOString(),
1657
1688
  expiresAt: new Date(expiresMs).toISOString(),
1658
- ticket: found.groups?.ticket ?? null,
1659
- detail: (found.groups?.reason ?? "").trim() || null,
1689
+ ticket: trailer.ticket,
1690
+ detail: trailer.reason,
1660
1691
  });
1661
1692
  }
1662
1693
 
@@ -1674,6 +1705,174 @@ const BYPASS_REJECTIONS = Object.freeze({
1674
1705
  "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.",
1675
1706
  });
1676
1707
 
1708
+ // ---------------------------------------------------------------------------
1709
+ // 4.1 The waiver record — durable, and RE-DERIVED at the moment of asking
1710
+ // ---------------------------------------------------------------------------
1711
+ //
1712
+ // Two defects live here, and they are the same defect seen from two ends.
1713
+ //
1714
+ // THE VERDICT WAS REPLAYED. The merge consumes a STORED check run. Between the
1715
+ // last evaluation and the merge, `bypassed` is a record of the past, and a
1716
+ // waiver is mutable pull-request state a human can edit. Removal at least fires
1717
+ // `unlabeled` and re-runs the gate; EXPIRY fires nothing at all, so the one
1718
+ // transition that produces no event is the one a replayed verdict can never
1719
+ // see. `runWaiverVerdict` re-derives the waiver from the LIVE pull request at
1720
+ // the moment the merge asks, through the SAME `observeWaiver` the gate itself
1721
+ // uses — not a second implementation that can drift into disagreeing with it.
1722
+ //
1723
+ // THE WAIVER LEFT NO RECORD. The label is the only repository-wide index of
1724
+ // "which merges went past this gate on a waiver", and the reaper strips it on
1725
+ // close, which withdraws the REQUEST and erases the INDEX with it. The record
1726
+ // below is written into a marker-delimited comment BEFORE the strip, so the
1727
+ // index outlives the label. `formatWaiverRecord` is deliberately buildable from
1728
+ // a REFUSED waiver too: an expired waiver still shipped something.
1729
+ //
1730
+ // Both halves refuse to guess. An unreadable pull request is `not_determined`
1731
+ // and never `waived` — "we could not check" must not render as "it is fine",
1732
+ // which is the rule the rest of this file is built on.
1733
+
1734
+ /**
1735
+ * The four answers to "is a waiver in force on this pull request right now?".
1736
+ *
1737
+ * `none` is not a synonym for `waived`: it means nobody asked for a waiver, so
1738
+ * whatever the gate reported rests on suite evidence alone. `not_determined` is
1739
+ * not a synonym for `refused` either — one is a fact about the waiver, the
1740
+ * other is the absence of any fact — and only `refused` names a remedy.
1741
+ */
1742
+ export const WAIVER_STATES = Object.freeze({
1743
+ none: "none",
1744
+ waived: "waived",
1745
+ refused: "refused",
1746
+ notDetermined: "not_determined",
1747
+ });
1748
+
1749
+ /**
1750
+ * The token every durable waiver record carries, and the key it is found by.
1751
+ *
1752
+ * The record has to be enumerable AFTER the reaper has stripped the label, so
1753
+ * the search key must be something the reaper does not touch. This string is
1754
+ * that key; the markers below wrap it.
1755
+ */
1756
+ export const WAIVER_RECORD_MARKER = "NIGHTLY-E2E-BYPASS-RECORD";
1757
+
1758
+ /** Opening marker of the durable waiver record. */
1759
+ export const WAIVER_RECORD_BEGIN = `<!-- ${WAIVER_RECORD_MARKER}-BEGIN -->`;
1760
+
1761
+ /** Closing marker of the durable waiver record. */
1762
+ export const WAIVER_RECORD_END = `<!-- ${WAIVER_RECORD_MARKER}-END -->`;
1763
+
1764
+ /**
1765
+ * Turns a bypass decision into the merge-time waiver state.
1766
+ *
1767
+ * @param {object|null|undefined} bypass - A decision from `evaluateBypass`, or `null` when no waiver was requested
1768
+ * @returns {string} One of `WAIVER_STATES`
1769
+ */
1770
+ export function classifyWaiver(bypass) {
1771
+ if (bypass === null || bypass === undefined) return WAIVER_STATES.none;
1772
+ if (bypass.reason === "pr_state_unreadable") {
1773
+ return WAIVER_STATES.notDetermined;
1774
+ }
1775
+ return bypass.valid === true ? WAIVER_STATES.waived : WAIVER_STATES.refused;
1776
+ }
1777
+
1778
+ /** One row of the record table, or nothing when the value is unknown. */
1779
+ const recordRow = (field, value) =>
1780
+ value === null || value === undefined || value === ""
1781
+ ? []
1782
+ : [`| ${field} | ${value} |`];
1783
+
1784
+ /**
1785
+ * Renders the durable waiver record as a marker-delimited comment body.
1786
+ *
1787
+ * The markers are what make it survivable and enumerable: a rewriter that
1788
+ * preserves marked regions cannot drop it by accident, and one that does not at
1789
+ * least leaves the deletion legible. They are also the search key — the record
1790
+ * outlives the label, so enumeration has to key on something the reaper does
1791
+ * not touch.
1792
+ *
1793
+ * Every unknown field is OMITTED rather than rendered as a confident empty
1794
+ * string. A record that prints `ticket:` with nothing after it reads as "there
1795
+ * was no ticket" when what happened is "the trailer could not be read".
1796
+ *
1797
+ * @param {{state: string, label: string, prNumber: number|null, gateContext: string, derivedAt: string, waiver: object|null, trailer: object|null, repo: string|null}} result - A `runWaiverVerdict` result
1798
+ * @returns {string} The comment body
1799
+ */
1800
+ export function formatWaiverRecord(result) {
1801
+ const waiver = result.waiver ?? {};
1802
+ return [
1803
+ WAIVER_RECORD_BEGIN,
1804
+ "### 🌙 Nightly E2E waiver record",
1805
+ "",
1806
+ `This pull request carried an audited waiver against the \`${result.gateContext}\` merge gate. The \`${result.label}\` label is stripped when the pull request closes, so THIS comment — not the label — is what makes the waiver enumerable afterwards.`,
1807
+ "",
1808
+ "| field | value |",
1809
+ "| --- | --- |",
1810
+ ...recordRow("label", `\`${result.label}\``),
1811
+ ...recordRow(
1812
+ "ticket",
1813
+ result.trailer?.ticket ? `\`${result.trailer.ticket}\`` : null
1814
+ ),
1815
+ ...recordRow("reason", result.trailer?.reason),
1816
+ ...recordRow("applied by", waiver.actor ? `\`${waiver.actor}\`` : null),
1817
+ ...recordRow("applied at", waiver.appliedAt),
1818
+ ...recordRow("expires at", waiver.expiresAt),
1819
+ ...recordRow("state when recorded", `\`${result.state}\``),
1820
+ ...recordRow(
1821
+ "refusal",
1822
+ result.state === WAIVER_STATES.refused
1823
+ ? (BYPASS_REJECTIONS[waiver.reason] ?? waiver.reason)
1824
+ : null
1825
+ ),
1826
+ ...recordRow("re-derived at", result.derivedAt),
1827
+ "",
1828
+ `**Search key:** \`${WAIVER_RECORD_MARKER}\`. Every waived merge in \`${result.repo ?? "this repository"}\` is the set of pull-request comments carrying it, paginated from the REST comment listing for the repository's issues. The reaper never touches this comment, which is the whole point: the label answers "is a waiver being requested", and only this answers "what shipped on a waiver, and why".`,
1829
+ WAIVER_RECORD_END,
1830
+ "",
1831
+ ].join("\n");
1832
+ }
1833
+
1834
+ /** The headline each waiver state reports. */
1835
+ const WAIVER_HEADLINES = Object.freeze({
1836
+ none: "✅ **No waiver is in force.** Nobody asked this gate to waive anything, so whatever it reported rests on suite evidence alone.",
1837
+ waived:
1838
+ "⚠️ **A valid waiver IS in force right now.** This is not the stored verdict being trusted — it was re-derived from the live pull request at the moment of asking.",
1839
+ refused:
1840
+ "⛔ **A waiver was requested and is NOT valid right now.** A stored `bypassed` verdict must not carry this merge: it is a record of the past, and the waiver behind it no longer holds.",
1841
+ not_determined:
1842
+ "⚠️ **NOT DETERMINED.** The live pull request could not be read, so whether a waiver is in force is UNKNOWN. This is not a pass — an answer that could not be measured must never render as one that was.",
1843
+ });
1844
+
1845
+ /**
1846
+ * Renders the merge-time waiver verdict for a human.
1847
+ *
1848
+ * @param {object} result - A `runWaiverVerdict` result
1849
+ * @returns {string} Markdown
1850
+ */
1851
+ export function formatWaiverVerdict(result) {
1852
+ const lines = [
1853
+ "## 🌙 Nightly E2E waiver — re-derived",
1854
+ "",
1855
+ WAIVER_HEADLINES[result.state],
1856
+ "",
1857
+ `Pull request: #${result.prNumber ?? "—"} · label \`${result.label}\` · re-derived at ${result.derivedAt}.`,
1858
+ ];
1859
+ if (result.state === WAIVER_STATES.refused) {
1860
+ lines.push(
1861
+ "",
1862
+ `**What the waiver covered:** the \`${result.gateContext}\` merge gate${result.trailer?.ticket ? `, under ticket \`${result.trailer.ticket}\`` : ""}${result.trailer?.reason ? ` — ${result.trailer.reason}` : ""}.`,
1863
+ "",
1864
+ `**Why it is refused now:** ${BYPASS_REJECTIONS[result.waiver?.reason] ?? result.waiver?.reason}`,
1865
+ "",
1866
+ `**Remedy:** fix the red suite, or re-apply a fresh waiver — remove the \`${result.label}\` label, confirm the \`Nightly-E2E-Bypass: <TICKET> <reason>\` line still says what you mean, and re-apply the label so a maintainer's grant is dated from now. Do not merge on the earlier green.`
1867
+ );
1868
+ }
1869
+ if (result.state === WAIVER_STATES.notDetermined) {
1870
+ lines.push("", `**Remedy:** ${BYPASS_REJECTIONS.pr_state_unreadable}`);
1871
+ }
1872
+ if (result.record) lines.push("", result.record);
1873
+ return `${lines.join("\n")}\n`;
1874
+ }
1875
+
1677
1876
  // ---------------------------------------------------------------------------
1678
1877
  // 5. Verdict
1679
1878
  // ---------------------------------------------------------------------------
@@ -2686,6 +2885,9 @@ const GITHUB_ACCEPT = "application/vnd.github+json";
2686
2885
  */
2687
2886
  const HEALTH_USER_AGENT = "lisa-nightly-e2e-health";
2688
2887
 
2888
+ /** GitHub's public API origin, used when the runner exports no override. */
2889
+ const DEFAULT_GITHUB_API_URL = "https://api.github.com";
2890
+
2689
2891
  /**
2690
2892
  * How long to wait before retrying a throttled response, bounded.
2691
2893
  *
@@ -3697,6 +3899,226 @@ export async function fetchActorPermission(api, login, wait) {
3697
3899
  return result?.body?.role_name ?? result?.body?.permission ?? null;
3698
3900
  }
3699
3901
 
3902
+ /**
3903
+ * The waiver question, extracted from resolved settings.
3904
+ *
3905
+ * @param {object} settings - Resolved settings carrying `pr`, `bypassLabel`, `bypassMaxHours`
3906
+ * @param {Date} now - The instant to evaluate at
3907
+ * @returns {object} The request `observeWaiver` consumes
3908
+ */
3909
+ function waiverRequest(settings, now) {
3910
+ return {
3911
+ prNumber: settings.pr.number,
3912
+ label: settings.bypassLabel,
3913
+ payloadAuthor: settings.pr.payloadAuthor,
3914
+ maxHours: settings.bypassMaxHours,
3915
+ extraReasonPattern: settings.extraBypassReasonPattern,
3916
+ now,
3917
+ };
3918
+ }
3919
+
3920
+ /**
3921
+ * Reads the pull request LIVE and derives the waiver from what it says NOW.
3922
+ *
3923
+ * The ONE place the waiver is derived. The gate calls it while producing the
3924
+ * check, and the merge-time re-derivation calls it again later; that they are
3925
+ * the same call is the property that makes "re-derived" mean anything, because
3926
+ * a second implementation would eventually answer a different question.
3927
+ *
3928
+ * Four outcomes, and the two that are easy to collapse are kept apart:
3929
+ *
3930
+ * no pull request `bypass: null`, `labelPresent: false` — nothing to waive
3931
+ * label absent `bypass: null`, `labelPresent: false` — nobody asked
3932
+ * unreadable a REJECTED bypass, `labelPresent: null` — UNKNOWN, never a grant
3933
+ * label present whatever `evaluateBypass` decides
3934
+ *
3935
+ * `trailer` is returned alongside, parsed from the live body regardless of
3936
+ * whether the waiver qualifies, because the durable record has to be able to
3937
+ * say what an EXPIRED waiver claimed.
3938
+ *
3939
+ * @param {object} api - API coordinates
3940
+ * @param {{prNumber: number|null, label: string, payloadAuthor: string|null, maxHours: number, extraReasonPattern: string, now: Date}} request - The waiver question
3941
+ * @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
3942
+ * @returns {Promise<{bypass: object|null, trailer: object|null, labelPresent: boolean|null}>} The observation
3943
+ */
3944
+ export async function observeWaiver(api, request, wait) {
3945
+ const absent = Object.freeze({
3946
+ bypass: null,
3947
+ trailer: null,
3948
+ labelPresent: false,
3949
+ });
3950
+ if (!request.prNumber) return absent;
3951
+
3952
+ const live = await fetchPullRequestState(api, request.prNumber, wait);
3953
+ const subject = {
3954
+ prNumber: request.prNumber,
3955
+ label: request.label,
3956
+ prAuthor: request.payloadAuthor,
3957
+ };
3958
+ if (live === null) {
3959
+ // Unreadable is UNKNOWN, and `labelPresent: null` says so in the same
3960
+ // vocabulary the bypass uses. Falling back to the event payload here is
3961
+ // exactly the defect row 40 exists to close.
3962
+ return Object.freeze({
3963
+ bypass: unreadablePullRequestBypass(subject),
3964
+ trailer: null,
3965
+ labelPresent: null,
3966
+ });
3967
+ }
3968
+ const trailer = parseWaiverTrailer(live.body);
3969
+ if (!live.labels.includes(request.label)) {
3970
+ // Read successfully, label not present. Nobody asked for a waiver, and a
3971
+ // label REMOVED since the run was triggered lands here rather than waiving
3972
+ // anything. The trailer is still carried: prose in a body is a request
3973
+ // nobody granted, not a waiver.
3974
+ return Object.freeze({ bypass: null, trailer, labelPresent: false });
3975
+ }
3976
+
3977
+ const labelEvent = await fetchLabelEvent(
3978
+ api,
3979
+ request.prNumber,
3980
+ request.label,
3981
+ wait
3982
+ );
3983
+ const actorPermission = labelEvent
3984
+ ? await fetchActorPermission(api, labelEvent.actor, wait)
3985
+ : null;
3986
+ return Object.freeze({
3987
+ bypass: evaluateBypass({
3988
+ labelEvent,
3989
+ prAuthor: live.author ?? request.payloadAuthor,
3990
+ prBody: live.body,
3991
+ actorPermission,
3992
+ prNumber: request.prNumber,
3993
+ label: request.label,
3994
+ maxHours: request.maxHours,
3995
+ extraReasonPattern: request.extraReasonPattern,
3996
+ now: request.now,
3997
+ }),
3998
+ trailer,
3999
+ labelPresent: true,
4000
+ });
4001
+ }
4002
+
4003
+ /**
4004
+ * Resolves everything the WAIVER half needs, and nothing else.
4005
+ *
4006
+ * Deliberately does not require `branch` or `suites`: the merge-time question
4007
+ * is "is the waiver still good?", not "is the nightly green?". Demanding the
4008
+ * suite table here would make the re-derivation impossible to run from the one
4009
+ * place that most needs it — a merge driver that has a pull request number and
4010
+ * a token, and no business restating the gate's configuration.
4011
+ *
4012
+ * The security ceilings are resolved through the SAME `resolveSecurityLimits`
4013
+ * the gate uses, so a caller cannot buy a longer waiver by asking a different
4014
+ * entry point.
4015
+ *
4016
+ * @param {NodeJS.ProcessEnv} env - The environment
4017
+ * @returns {object} Resolved waiver settings
4018
+ * @throws {GateConfigError} When required settings are absent or unusable
4019
+ */
4020
+ export function resolveWaiverSettings(env) {
4021
+ const token = env.GITHUB_TOKEN || env.GH_TOKEN;
4022
+ if (!token) {
4023
+ throw new GateConfigError(
4024
+ "No GITHUB_TOKEN / GH_TOKEN in the environment. Re-deriving a waiver means reading the live pull request, and that needs a token."
4025
+ );
4026
+ }
4027
+ if (!env.GITHUB_REPOSITORY) {
4028
+ throw new GateConfigError(
4029
+ "No GITHUB_REPOSITORY in the environment, so there is no pull request to re-derive a waiver from."
4030
+ );
4031
+ }
4032
+ const prNumber = Number(env.NIGHTLY_PR_NUMBER) || null;
4033
+ if (!prNumber) {
4034
+ throw new GateConfigError(
4035
+ "No NIGHTLY_PR_NUMBER in the environment. A waiver is a property of one pull request; without one there is nothing to re-derive, and answering `none` here would be a guess wearing a verdict's clothes."
4036
+ );
4037
+ }
4038
+ const raw = env.NIGHTLY_BYPASS_MAX_HOURS;
4039
+ const requestedHours =
4040
+ raw === undefined || String(raw).trim() === "" ? 24 : Number(raw);
4041
+ if (!Number.isFinite(requestedHours) || requestedHours <= 0) {
4042
+ throw new GateConfigError(
4043
+ `\`NIGHTLY_BYPASS_MAX_HOURS\` must be a positive number (got ${raw}).`
4044
+ );
4045
+ }
4046
+ const { limits } = resolveSecurityLimits({
4047
+ bypassMaxHours: requestedHours,
4048
+ bootstrapMaxDays: BOOTSTRAP_ABSOLUTE_MAX_DAYS,
4049
+ freshnessHours: ABSOLUTE_MAX_FRESHNESS_HOURS,
4050
+ apiMaxAttempts: ABSOLUTE_MAX_API_ATTEMPTS,
4051
+ apiMaxPages: ABSOLUTE_MAX_API_PAGES,
4052
+ apiRetryMaxSeconds: ABSOLUTE_MAX_RETRY_SECONDS,
4053
+ });
4054
+ return {
4055
+ api: {
4056
+ apiUrl: env.GITHUB_API_URL || DEFAULT_GITHUB_API_URL,
4057
+ repo: env.GITHUB_REPOSITORY,
4058
+ token,
4059
+ maxAttempts: limits.apiMaxAttempts,
4060
+ maxPages: limits.apiMaxPages,
4061
+ retryMaxSeconds: limits.apiRetryMaxSeconds,
4062
+ },
4063
+ gateContext:
4064
+ (env.NIGHTLY_GATE_CONTEXT ?? "").trim() || DEFAULT_GATE_CONTEXT,
4065
+ bypassLabel:
4066
+ String(env.NIGHTLY_BYPASS_LABEL ?? "").trim() || DEFAULT_BYPASS_LABEL,
4067
+ bypassMaxHours: limits.bypassMaxHours,
4068
+ extraBypassReasonPattern: env.NIGHTLY_BYPASS_REASON_PATTERN || "",
4069
+ pr: { number: prNumber, payloadAuthor: env.NIGHTLY_PR_AUTHOR || null },
4070
+ };
4071
+ }
4072
+
4073
+ /**
4074
+ * Re-derives the waiver from the live pull request, at the moment of asking.
4075
+ *
4076
+ * This is the answer to "the verdict is replayed, not re-derived". A stored
4077
+ * check run says what was true when it ran; this says what is true now. The
4078
+ * difference matters for exactly one transition — EXPIRY — because every other
4079
+ * change to a waiver (applying the label, removing it, editing the body) fires
4080
+ * a pull-request event the gate already subscribes to, and expiry fires
4081
+ * nothing. A waiver that lapses between the last evaluation and the merge is
4082
+ * invisible to any amount of re-running; it is only visible to re-deriving.
4083
+ *
4084
+ * @param {NodeJS.ProcessEnv} env - The environment
4085
+ * @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
4086
+ * @returns {Promise<object>} The waiver verdict, with its durable record
4087
+ */
4088
+ export async function runWaiverVerdict(env, wait) {
4089
+ const settings = resolveWaiverSettings(env);
4090
+ const now = new Date();
4091
+ const observed = await observeWaiver(
4092
+ settings.api,
4093
+ waiverRequest(settings, now),
4094
+ wait
4095
+ );
4096
+ const state = classifyWaiver(observed.bypass);
4097
+ const result = {
4098
+ state,
4099
+ prNumber: settings.pr.number,
4100
+ repo: settings.api.repo,
4101
+ label: settings.bypassLabel,
4102
+ gateContext: settings.gateContext,
4103
+ derivedAt: now.toISOString(),
4104
+ waiver: observed.bypass,
4105
+ trailer: observed.trailer,
4106
+ labelPresent: observed.labelPresent,
4107
+ record: null,
4108
+ };
4109
+ // A record is written for every state that involved a waiver at all —
4110
+ // including a REFUSED one. "What shipped on a waiver, and why" is exactly the
4111
+ // question an expired waiver makes hard to answer, so it is the one the
4112
+ // record must still answer.
4113
+ return Object.freeze({
4114
+ ...result,
4115
+ record:
4116
+ state === WAIVER_STATES.none || state === WAIVER_STATES.notDetermined
4117
+ ? null
4118
+ : formatWaiverRecord(result),
4119
+ });
4120
+ }
4121
+
3700
4122
  // ---------------------------------------------------------------------------
3701
4123
  // 8. Entry point
3702
4124
  // ---------------------------------------------------------------------------
@@ -3754,7 +4176,7 @@ export function resolveSettings(env) {
3754
4176
 
3755
4177
  return {
3756
4178
  api: {
3757
- apiUrl: env.GITHUB_API_URL || "https://api.github.com",
4179
+ apiUrl: env.GITHUB_API_URL || DEFAULT_GITHUB_API_URL,
3758
4180
  repo: env.GITHUB_REPOSITORY,
3759
4181
  token,
3760
4182
  maxAttempts: limits.apiMaxAttempts,
@@ -3767,7 +4189,7 @@ export function resolveSettings(env) {
3767
4189
  // the runner already exports the right value.
3768
4190
  graphqlUrl:
3769
4191
  env.GITHUB_GRAPHQL_URL ||
3770
- `${env.GITHUB_API_URL || "https://api.github.com"}/graphql`,
4192
+ `${env.GITHUB_API_URL || DEFAULT_GITHUB_API_URL}/graphql`,
3771
4193
  },
3772
4194
  issueLabel: env.NIGHTLY_ISSUE_LABEL || TRACKING_ISSUE_LABEL,
3773
4195
  // Reporting-only settings. The gate reads none of them, which is why they
@@ -3865,47 +4287,16 @@ export async function runGate(env, wait) {
3865
4287
  // The bypass reads the pull request LIVE and never consults `github.event`.
3866
4288
  // Three outcomes, and the middle one is the vacuity guard: unreadable is a
3867
4289
  // REJECTED bypass, never an absent one and never a granted one.
3868
- let bypass = null;
3869
- if (settings.pr.number) {
3870
- const live = await fetchPullRequestState(
3871
- settings.api,
3872
- settings.pr.number,
3873
- wait
3874
- );
3875
- const subject = {
3876
- prNumber: settings.pr.number,
3877
- label: settings.bypassLabel,
3878
- prAuthor: settings.pr.payloadAuthor,
3879
- };
3880
- if (live === null) {
3881
- bypass = unreadablePullRequestBypass(subject);
3882
- } else if (live.labels.includes(settings.bypassLabel)) {
3883
- const labelEvent = await fetchLabelEvent(
3884
- settings.api,
3885
- settings.pr.number,
3886
- settings.bypassLabel,
3887
- wait
3888
- );
3889
- const actorPermission = labelEvent
3890
- ? await fetchActorPermission(settings.api, labelEvent.actor, wait)
3891
- : null;
3892
- bypass = evaluateBypass({
3893
- labelEvent,
3894
- prAuthor: live.author ?? settings.pr.payloadAuthor,
3895
- prBody: live.body,
3896
- actorPermission,
3897
- prNumber: settings.pr.number,
3898
- label: settings.bypassLabel,
3899
- maxHours: settings.bypassMaxHours,
3900
- extraReasonPattern: settings.extraBypassReasonPattern,
3901
- now,
3902
- });
3903
- }
3904
- // The remaining case — read successfully, label not present — leaves
3905
- // `bypass` null. Nobody asked for a waiver, so there is nothing to report,
3906
- // and a label REMOVED since the run was triggered lands here rather than
3907
- // waiving anything.
3908
- }
4290
+ //
4291
+ // Derived through `observeWaiver`, which is the SAME call the merge-time
4292
+ // re-derivation makes. Two implementations of "is this waiver good?" would
4293
+ // drift, and the whole value of re-deriving at merge time is that it answers
4294
+ // the question the gate answered — later, not differently.
4295
+ const { bypass } = await observeWaiver(
4296
+ settings.api,
4297
+ waiverRequest(settings, now),
4298
+ wait
4299
+ );
3909
4300
 
3910
4301
  return {
3911
4302
  ...decide(findings, { bootstrap, bypass }),
@@ -4213,6 +4604,10 @@ async function main(argv) {
4213
4604
  // The reporting half is opt-in at the call site, which is what keeps the
4214
4605
  // default invocation — the required status check — provably read-only.
4215
4606
  if (argv.includes("--report-issues")) return await reportIssues(asJson);
4607
+ // The merge-time re-derivation. Also read-only: it emits the durable record
4608
+ // rather than posting it, so the one caller that needs `pull-requests: write`
4609
+ // (the reaper) is the only place a write can happen.
4610
+ if (argv.includes("--waiver-verdict")) return await waiverVerdict(asJson);
4216
4611
 
4217
4612
  /** @type {object} */
4218
4613
  let verdict;
@@ -4277,6 +4672,85 @@ async function main(argv) {
4277
4672
  }
4278
4673
  }
4279
4674
 
4675
+ /**
4676
+ * The `--waiver-verdict` CLI: re-derive the waiver and say what it found.
4677
+ *
4678
+ * The exit code is the merge-time contract, and it has three arms rather than
4679
+ * two. `none` and `waived` exit 0 — the first because nobody asked for a
4680
+ * waiver, the second because a genuine, still-valid waiver is the escape hatch
4681
+ * working as designed and must keep merging. `refused` and `not_determined`
4682
+ * both exit 1, and they say DIFFERENT things: one names a lapsed waiver and its
4683
+ * remedy, the other says the question could not be answered. Collapsing the
4684
+ * second into a pass is the failure this whole mode exists to prevent.
4685
+ *
4686
+ * Exported for the same reason `reportIssues` is: the exit code IS the
4687
+ * contract here — it is what stops a merge — and a contract only a spawned
4688
+ * process can observe is one the suite tests indirectly or not at all.
4689
+ *
4690
+ * @param {boolean} asJson - Whether to emit the machine record instead
4691
+ * @returns {Promise<void>} Resolves once the verdict is written
4692
+ */
4693
+ export async function waiverVerdict(asJson) {
4694
+ /** @type {object} */
4695
+ let result;
4696
+ try {
4697
+ result = await runWaiverVerdict(process.env);
4698
+ } catch (error) {
4699
+ const message = error instanceof Error ? error.message : String(error);
4700
+ const failure = {
4701
+ state: WAIVER_STATES.notDetermined,
4702
+ error: {
4703
+ kind: error instanceof GateConfigError ? "configuration" : "api",
4704
+ message,
4705
+ },
4706
+ record: null,
4707
+ };
4708
+ if (asJson) {
4709
+ process.stdout.write(`${JSON.stringify(failure, null, 2)}\n`);
4710
+ } else {
4711
+ process.stdout.write(
4712
+ `## 🌙 Nightly E2E waiver — re-derived\n\n⚠️ **NOT DETERMINED.** ${message}\n\nThis is not a pass. A waiver whose state could not be re-derived must not carry a merge.\n`
4713
+ );
4714
+ }
4715
+ await writeWaiverOutputs(failure);
4716
+ process.exitCode = 1;
4717
+ return;
4718
+ }
4719
+
4720
+ if (asJson) {
4721
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
4722
+ } else {
4723
+ const report = formatWaiverVerdict(result);
4724
+ process.stdout.write(report);
4725
+ await appendSummary(report);
4726
+ }
4727
+ await writeWaiverOutputs(result);
4728
+ if (
4729
+ result.state === WAIVER_STATES.refused ||
4730
+ result.state === WAIVER_STATES.notDetermined
4731
+ ) {
4732
+ process.exitCode = 1;
4733
+ }
4734
+ }
4735
+
4736
+ /**
4737
+ * Publishes the waiver verdict as step outputs.
4738
+ *
4739
+ * `waiver_record` is emitted as a heredoc so the reaper can post it verbatim
4740
+ * without the record's own markdown having to survive a shell quote.
4741
+ *
4742
+ * @param {object} result - The waiver verdict
4743
+ * @returns {Promise<void>} Resolves when written
4744
+ */
4745
+ async function writeWaiverOutputs(result) {
4746
+ if (!process.env.GITHUB_OUTPUT) return;
4747
+ const { appendFileSync } = await import("node:fs");
4748
+ appendFileSync(
4749
+ process.env.GITHUB_OUTPUT,
4750
+ `waiver_state=${result.state}\nwaiver_record<<LISA_WAIVER_EOF\n${result.record ?? ""}\nLISA_WAIVER_EOF\n`
4751
+ );
4752
+ }
4753
+
4280
4754
  /**
4281
4755
  * Appends a report to the job summary when one exists.
4282
4756
  *