@codyswann/lisa 4.55.2 → 4.55.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/all/copy-overwrite/scripts/check-orphaned-branches.mjs +49 -13
  2. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +9 -1
  3. package/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +10 -5
  4. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +4 -0
  5. package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +73 -2
  6. package/all/copy-overwrite/scripts/lisa-hooks/guard-dedupe.bash +63 -29
  7. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +2 -1
  8. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.sh +2 -1
  9. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +7 -2
  10. package/all/copy-overwrite/scripts/lisa-work-item.mjs +50 -14
  11. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  12. package/dist/core/lisa-owned-hash-ledger.js +36 -0
  13. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  14. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  15. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  16. package/dist/core/upstream-evidence-manifest.js +92 -80
  17. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  18. package/dist/opencode/plugin-templates/block-managed-file-edits.sh +76 -2
  19. package/dist/opencode/plugin-templates/block-no-verify.sh +3 -0
  20. package/dist/opencode/plugin-templates/guard-dedupe.bash +66 -29
  21. package/dist/opencode/plugin-templates/parity-safety-net-heredoc.py +3 -0
  22. package/dist/opencode/plugin-templates/parity-safety-net.sh +5 -1
  23. package/dist/opencode/support-file-resolver.d.ts.map +1 -1
  24. package/dist/opencode/support-file-resolver.js +3 -1
  25. package/dist/opencode/support-file-resolver.js.map +1 -1
  26. package/expo/copy-overwrite/plugins/withAndroidSplashNoClientExit.js +23 -21
  27. package/expo/create-only/.github/required-checks.json +1 -1
  28. package/nestjs/create-only/.github/required-checks.json +1 -1
  29. package/package.json +4 -4
  30. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +38 -15
  33. package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +15 -1
  34. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +37 -13
  35. package/plugins/lisa/.codex-plugin/skills/lisa-jira-write-ticket/SKILL.md +15 -1
  36. package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +56 -7
  37. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +36 -11
  38. package/plugins/lisa/.codex-plugin/skills/lisa-linear-prd-intake/SKILL.md +12 -2
  39. package/plugins/lisa/.codex-plugin/skills/lisa-linear-validate-issue/SKILL.md +1 -18
  40. package/plugins/lisa/.codex-plugin/skills/lisa-linear-verify/SKILL.md +0 -17
  41. package/plugins/lisa/.codex-plugin/skills/lisa-linear-write-issue/SKILL.md +18 -35
  42. package/plugins/lisa/.codex-plugin/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  43. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +66 -12
  44. package/plugins/lisa/.codex-plugin/skills/lisa-rework-triage/SKILL.md +2 -1
  45. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
  46. package/plugins/lisa/.codex-plugin/skills/lisa-setup-linear/SKILL.md +1 -1
  47. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
  48. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
  49. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
  50. package/plugins/lisa/.codex-plugin/skills/lisa-track/SKILL.md +32 -4
  51. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-build-intake/SKILL.md +15 -1
  52. package/plugins/lisa/hooks/block-blind-automerge.sh +13 -5
  53. package/plugins/lisa/hooks/block-direct-issue-create.sh +7 -0
  54. package/plugins/lisa/hooks/block-instruction-file-edits.sh +3 -0
  55. package/plugins/lisa/hooks/block-managed-file-edits.sh +76 -2
  56. package/plugins/lisa/hooks/block-no-verify.sh +3 -0
  57. package/plugins/lisa/hooks/block-shell-json-parsing.sh +3 -0
  58. package/plugins/lisa/hooks/guard-dedupe.bash +66 -29
  59. package/plugins/lisa/hooks/parity-safety-net-heredoc.py +3 -0
  60. package/plugins/lisa/hooks/parity-safety-net.sh +5 -1
  61. package/plugins/lisa/hooks/worktree-binding-guard.mjs +3 -0
  62. package/plugins/lisa/hooks/worktree-binding-guard.sh +5 -1
  63. package/plugins/lisa/rules/eager/00-rule-index.md +1 -0
  64. package/plugins/lisa/rules/eager/base-rules.md +3 -1
  65. package/plugins/lisa/rules/eager/withdrawn-rulings.md +14 -3
  66. package/plugins/lisa/rules/reference/automation-runbook-contract.md +1 -1
  67. package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +1 -1
  68. package/plugins/lisa/rules/reference/blocker-containment.md +3 -3
  69. package/plugins/lisa/rules/reference/claim-archaeology.md +1 -1
  70. package/plugins/lisa/rules/reference/claim-evidence-mapping.md +1 -1
  71. package/plugins/lisa/rules/reference/claim-time-guards.md +1 -1
  72. package/plugins/lisa/rules/reference/control-reachability.md +1 -1
  73. package/plugins/lisa/rules/reference/convergent-review.md +1 -1
  74. package/plugins/lisa/rules/reference/credential-substrate-precedence.md +1 -1
  75. package/plugins/lisa/rules/reference/dependency-decision-records.md +1 -1
  76. package/plugins/lisa/rules/reference/dependency-internalization-kit.md +1 -1
  77. package/plugins/lisa/rules/reference/dependency-trust-classes.md +1 -1
  78. package/plugins/lisa/rules/reference/deployed-state-readback.md +1 -1
  79. package/plugins/lisa/rules/reference/derived-branch-plan.md +1 -1
  80. package/plugins/lisa/rules/reference/design-source-of-truth.md +1 -1
  81. package/plugins/lisa/rules/reference/design-value-binding.md +1 -1
  82. package/plugins/lisa/rules/reference/documentation-source-paths.md +1 -1
  83. package/plugins/lisa/rules/reference/leaf-only-lifecycle.md +1 -1
  84. package/plugins/lisa/rules/reference/observability-audit.md +1 -1
  85. package/plugins/lisa/rules/reference/prd-definition-of-ready.md +1 -1
  86. package/plugins/lisa/rules/reference/prd-lifecycle-rollup.md +1 -1
  87. package/plugins/lisa/rules/reference/pre-flight-autofill.md +1 -1
  88. package/plugins/lisa/rules/reference/promotion-contract.md +1 -1
  89. package/plugins/lisa/rules/reference/readiness-rubric.md +1 -1
  90. package/plugins/lisa/rules/reference/ready-role-filing.md +37 -1
  91. package/plugins/lisa/rules/reference/rejection-detection.md +1 -1
  92. package/plugins/lisa/rules/reference/repo-scope-split.md +9 -10
  93. package/plugins/lisa/rules/reference/reset-seed-coverage.md +1 -1
  94. package/plugins/lisa/rules/reference/usage-accounting.md +1 -1
  95. package/plugins/lisa/rules/reference/work-item-definition-of-ready.md +1 -1
  96. package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +85 -17
  97. package/plugins/lisa/scripts/qa-signal-lifecycle.mjs +5 -2
  98. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +38 -15
  99. package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +15 -1
  100. package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +37 -13
  101. package/plugins/lisa/skills/lisa-jira-write-ticket/SKILL.md +15 -1
  102. package/plugins/lisa/skills/lisa-linear-access/SKILL.md +56 -7
  103. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +36 -11
  104. package/plugins/lisa/skills/lisa-linear-prd-intake/SKILL.md +12 -2
  105. package/plugins/lisa/skills/lisa-linear-validate-issue/SKILL.md +1 -18
  106. package/plugins/lisa/skills/lisa-linear-verify/SKILL.md +0 -17
  107. package/plugins/lisa/skills/lisa-linear-write-issue/SKILL.md +18 -35
  108. package/plugins/lisa/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  109. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +66 -12
  110. package/plugins/lisa/skills/lisa-rework-triage/SKILL.md +2 -1
  111. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
  112. package/plugins/lisa/skills/lisa-setup-linear/SKILL.md +1 -1
  113. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
  114. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
  115. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
  116. package/plugins/lisa/skills/lisa-track/SKILL.md +32 -4
  117. package/plugins/lisa/skills/lisa-tracker-build-intake/SKILL.md +15 -1
  118. package/plugins/lisa-agy/hooks/block-blind-automerge.sh +13 -5
  119. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +7 -0
  120. package/plugins/lisa-agy/hooks/block-instruction-file-edits.sh +3 -0
  121. package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +76 -2
  122. package/plugins/lisa-agy/hooks/block-shell-json-parsing.sh +3 -0
  123. package/plugins/lisa-agy/hooks/guard-dedupe.bash +66 -29
  124. package/plugins/lisa-agy/hooks/parity-safety-net-heredoc.py +3 -0
  125. package/plugins/lisa-agy/hooks/parity-safety-net.sh +5 -1
  126. package/plugins/lisa-agy/plugin.json +1 -1
  127. package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +85 -17
  128. package/plugins/lisa-agy/scripts/qa-signal-lifecycle.mjs +5 -2
  129. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +38 -15
  130. package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +15 -1
  131. package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +37 -13
  132. package/plugins/lisa-agy/skills/lisa-jira-write-ticket/SKILL.md +15 -1
  133. package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +56 -7
  134. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +36 -11
  135. package/plugins/lisa-agy/skills/lisa-linear-prd-intake/SKILL.md +12 -2
  136. package/plugins/lisa-agy/skills/lisa-linear-validate-issue/SKILL.md +1 -18
  137. package/plugins/lisa-agy/skills/lisa-linear-verify/SKILL.md +0 -17
  138. package/plugins/lisa-agy/skills/lisa-linear-write-issue/SKILL.md +18 -35
  139. package/plugins/lisa-agy/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  140. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +66 -12
  141. package/plugins/lisa-agy/skills/lisa-rework-triage/SKILL.md +2 -1
  142. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
  143. package/plugins/lisa-agy/skills/lisa-setup-linear/SKILL.md +1 -1
  144. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
  145. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
  146. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
  147. package/plugins/lisa-agy/skills/lisa-track/SKILL.md +32 -4
  148. package/plugins/lisa-agy/skills/lisa-tracker-build-intake/SKILL.md +15 -1
  149. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  150. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  151. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  152. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  154. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  155. package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +13 -5
  156. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +7 -0
  157. package/plugins/lisa-copilot/hooks/block-instruction-file-edits.sh +3 -0
  158. package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +76 -2
  159. package/plugins/lisa-copilot/hooks/block-no-verify.sh +3 -0
  160. package/plugins/lisa-copilot/hooks/block-shell-json-parsing.sh +3 -0
  161. package/plugins/lisa-copilot/hooks/guard-dedupe.bash +66 -29
  162. package/plugins/lisa-copilot/hooks/parity-safety-net-heredoc.py +3 -0
  163. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +5 -1
  164. package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +3 -0
  165. package/plugins/lisa-copilot/hooks/worktree-binding-guard.sh +5 -1
  166. package/plugins/lisa-copilot/rules/eager/00-rule-index.md +1 -0
  167. package/plugins/lisa-copilot/rules/eager/base-rules.md +3 -1
  168. package/plugins/lisa-copilot/rules/eager/withdrawn-rulings.md +14 -3
  169. package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +1 -1
  170. package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +1 -1
  171. package/plugins/lisa-copilot/rules/reference/blocker-containment.md +3 -3
  172. package/plugins/lisa-copilot/rules/reference/claim-archaeology.md +1 -1
  173. package/plugins/lisa-copilot/rules/reference/claim-evidence-mapping.md +1 -1
  174. package/plugins/lisa-copilot/rules/reference/claim-time-guards.md +1 -1
  175. package/plugins/lisa-copilot/rules/reference/control-reachability.md +1 -1
  176. package/plugins/lisa-copilot/rules/reference/convergent-review.md +1 -1
  177. package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +1 -1
  178. package/plugins/lisa-copilot/rules/reference/dependency-decision-records.md +1 -1
  179. package/plugins/lisa-copilot/rules/reference/dependency-internalization-kit.md +1 -1
  180. package/plugins/lisa-copilot/rules/reference/dependency-trust-classes.md +1 -1
  181. package/plugins/lisa-copilot/rules/reference/deployed-state-readback.md +1 -1
  182. package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +1 -1
  183. package/plugins/lisa-copilot/rules/reference/design-source-of-truth.md +1 -1
  184. package/plugins/lisa-copilot/rules/reference/design-value-binding.md +1 -1
  185. package/plugins/lisa-copilot/rules/reference/documentation-source-paths.md +1 -1
  186. package/plugins/lisa-copilot/rules/reference/leaf-only-lifecycle.md +1 -1
  187. package/plugins/lisa-copilot/rules/reference/observability-audit.md +1 -1
  188. package/plugins/lisa-copilot/rules/reference/prd-definition-of-ready.md +1 -1
  189. package/plugins/lisa-copilot/rules/reference/prd-lifecycle-rollup.md +1 -1
  190. package/plugins/lisa-copilot/rules/reference/pre-flight-autofill.md +1 -1
  191. package/plugins/lisa-copilot/rules/reference/promotion-contract.md +1 -1
  192. package/plugins/lisa-copilot/rules/reference/readiness-rubric.md +1 -1
  193. package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +37 -1
  194. package/plugins/lisa-copilot/rules/reference/rejection-detection.md +1 -1
  195. package/plugins/lisa-copilot/rules/reference/repo-scope-split.md +9 -10
  196. package/plugins/lisa-copilot/rules/reference/reset-seed-coverage.md +1 -1
  197. package/plugins/lisa-copilot/rules/reference/usage-accounting.md +1 -1
  198. package/plugins/lisa-copilot/rules/reference/work-item-definition-of-ready.md +1 -1
  199. package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +85 -17
  200. package/plugins/lisa-copilot/scripts/qa-signal-lifecycle.mjs +5 -2
  201. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +38 -15
  202. package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +15 -1
  203. package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +37 -13
  204. package/plugins/lisa-copilot/skills/lisa-jira-write-ticket/SKILL.md +15 -1
  205. package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +56 -7
  206. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +36 -11
  207. package/plugins/lisa-copilot/skills/lisa-linear-prd-intake/SKILL.md +12 -2
  208. package/plugins/lisa-copilot/skills/lisa-linear-validate-issue/SKILL.md +1 -18
  209. package/plugins/lisa-copilot/skills/lisa-linear-verify/SKILL.md +0 -17
  210. package/plugins/lisa-copilot/skills/lisa-linear-write-issue/SKILL.md +18 -35
  211. package/plugins/lisa-copilot/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  212. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +66 -12
  213. package/plugins/lisa-copilot/skills/lisa-rework-triage/SKILL.md +2 -1
  214. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
  215. package/plugins/lisa-copilot/skills/lisa-setup-linear/SKILL.md +1 -1
  216. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
  217. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
  218. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
  219. package/plugins/lisa-copilot/skills/lisa-track/SKILL.md +32 -4
  220. package/plugins/lisa-copilot/skills/lisa-tracker-build-intake/SKILL.md +15 -1
  221. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  222. package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +13 -5
  223. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +7 -0
  224. package/plugins/lisa-cursor/hooks/block-instruction-file-edits.sh +3 -0
  225. package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +76 -2
  226. package/plugins/lisa-cursor/hooks/block-no-verify.sh +3 -0
  227. package/plugins/lisa-cursor/hooks/block-shell-json-parsing.sh +3 -0
  228. package/plugins/lisa-cursor/hooks/guard-dedupe.bash +66 -29
  229. package/plugins/lisa-cursor/hooks/parity-safety-net-heredoc.py +3 -0
  230. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +5 -1
  231. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +3 -0
  232. package/plugins/lisa-cursor/hooks/worktree-binding-guard.sh +5 -1
  233. package/plugins/lisa-cursor/rules/00-rule-index.mdc +1 -0
  234. package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +1 -1
  235. package/plugins/lisa-cursor/rules/base-rules.mdc +3 -1
  236. package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +1 -1
  237. package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +3 -3
  238. package/plugins/lisa-cursor/rules/claim-archaeology-reference.mdc +1 -1
  239. package/plugins/lisa-cursor/rules/claim-evidence-mapping-reference.mdc +1 -1
  240. package/plugins/lisa-cursor/rules/claim-time-guards-reference.mdc +1 -1
  241. package/plugins/lisa-cursor/rules/control-reachability-reference.mdc +1 -1
  242. package/plugins/lisa-cursor/rules/convergent-review-reference.mdc +1 -1
  243. package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +1 -1
  244. package/plugins/lisa-cursor/rules/dependency-decision-records-reference.mdc +1 -1
  245. package/plugins/lisa-cursor/rules/dependency-internalization-kit-reference.mdc +1 -1
  246. package/plugins/lisa-cursor/rules/dependency-trust-classes-reference.mdc +1 -1
  247. package/plugins/lisa-cursor/rules/deployed-state-readback-reference.mdc +1 -1
  248. package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +1 -1
  249. package/plugins/lisa-cursor/rules/design-source-of-truth-reference.mdc +1 -1
  250. package/plugins/lisa-cursor/rules/design-value-binding-reference.mdc +1 -1
  251. package/plugins/lisa-cursor/rules/documentation-source-paths-reference.mdc +1 -1
  252. package/plugins/lisa-cursor/rules/leaf-only-lifecycle-reference.mdc +1 -1
  253. package/plugins/lisa-cursor/rules/observability-audit-reference.mdc +1 -1
  254. package/plugins/lisa-cursor/rules/prd-definition-of-ready-reference.mdc +1 -1
  255. package/plugins/lisa-cursor/rules/prd-lifecycle-rollup-reference.mdc +1 -1
  256. package/plugins/lisa-cursor/rules/pre-flight-autofill-reference.mdc +1 -1
  257. package/plugins/lisa-cursor/rules/promotion-contract-reference.mdc +1 -1
  258. package/plugins/lisa-cursor/rules/readiness-rubric-reference.mdc +1 -1
  259. package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +37 -1
  260. package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +1 -1
  261. package/plugins/lisa-cursor/rules/repo-scope-split-reference.mdc +9 -10
  262. package/plugins/lisa-cursor/rules/reset-seed-coverage-reference.mdc +1 -1
  263. package/plugins/lisa-cursor/rules/usage-accounting-reference.mdc +1 -1
  264. package/plugins/lisa-cursor/rules/withdrawn-rulings.mdc +14 -3
  265. package/plugins/lisa-cursor/rules/work-item-definition-of-ready-reference.mdc +1 -1
  266. package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +85 -17
  267. package/plugins/lisa-cursor/scripts/qa-signal-lifecycle.mjs +5 -2
  268. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +38 -15
  269. package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +15 -1
  270. package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +37 -13
  271. package/plugins/lisa-cursor/skills/lisa-jira-write-ticket/SKILL.md +15 -1
  272. package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +56 -7
  273. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +36 -11
  274. package/plugins/lisa-cursor/skills/lisa-linear-prd-intake/SKILL.md +12 -2
  275. package/plugins/lisa-cursor/skills/lisa-linear-validate-issue/SKILL.md +1 -18
  276. package/plugins/lisa-cursor/skills/lisa-linear-verify/SKILL.md +0 -17
  277. package/plugins/lisa-cursor/skills/lisa-linear-write-issue/SKILL.md +18 -35
  278. package/plugins/lisa-cursor/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  279. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +66 -12
  280. package/plugins/lisa-cursor/skills/lisa-rework-triage/SKILL.md +2 -1
  281. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
  282. package/plugins/lisa-cursor/skills/lisa-setup-linear/SKILL.md +1 -1
  283. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
  284. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
  285. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
  286. package/plugins/lisa-cursor/skills/lisa-track/SKILL.md +32 -4
  287. package/plugins/lisa-cursor/skills/lisa-tracker-build-intake/SKILL.md +15 -1
  288. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  289. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  290. package/plugins/lisa-expo-agy/plugin.json +1 -1
  291. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  292. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  293. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  294. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  295. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  296. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  297. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  298. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  299. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  300. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  301. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  302. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  303. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  304. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  305. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  306. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  307. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  308. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  309. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  310. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  311. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  312. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  313. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  314. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  315. package/plugins/lisa-rails-agy/plugin.json +1 -1
  316. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  317. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  318. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  319. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  320. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  321. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  322. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  323. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  324. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  325. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  326. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  327. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  328. package/plugins/materialized-artifacts.json +11 -0
  329. package/plugins/src/base/hooks/block-blind-automerge.sh +10 -5
  330. package/plugins/src/base/hooks/block-direct-issue-create.sh +4 -0
  331. package/plugins/src/base/hooks/block-managed-file-edits.sh +73 -2
  332. package/plugins/src/base/hooks/guard-dedupe.bash +63 -29
  333. package/plugins/src/base/hooks/parity-safety-net.sh +2 -1
  334. package/plugins/src/base/hooks/worktree-binding-guard.sh +2 -1
  335. package/plugins/src/base/rules/eager/00-rule-index.md +1 -0
  336. package/plugins/src/base/rules/eager/base-rules.md +3 -1
  337. package/plugins/src/base/rules/eager/withdrawn-rulings.md +14 -3
  338. package/plugins/src/base/rules/reference/automation-runbook-contract.md +1 -1
  339. package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +1 -1
  340. package/plugins/src/base/rules/reference/blocker-containment.md +3 -3
  341. package/plugins/src/base/rules/reference/claim-archaeology.md +1 -1
  342. package/plugins/src/base/rules/reference/claim-evidence-mapping.md +1 -1
  343. package/plugins/src/base/rules/reference/claim-time-guards.md +1 -1
  344. package/plugins/src/base/rules/reference/control-reachability.md +1 -1
  345. package/plugins/src/base/rules/reference/convergent-review.md +1 -1
  346. package/plugins/src/base/rules/reference/credential-substrate-precedence.md +1 -1
  347. package/plugins/src/base/rules/reference/dependency-decision-records.md +1 -1
  348. package/plugins/src/base/rules/reference/dependency-internalization-kit.md +1 -1
  349. package/plugins/src/base/rules/reference/dependency-trust-classes.md +1 -1
  350. package/plugins/src/base/rules/reference/deployed-state-readback.md +1 -1
  351. package/plugins/src/base/rules/reference/derived-branch-plan.md +1 -1
  352. package/plugins/src/base/rules/reference/design-source-of-truth.md +1 -1
  353. package/plugins/src/base/rules/reference/design-value-binding.md +1 -1
  354. package/plugins/src/base/rules/reference/documentation-source-paths.md +1 -1
  355. package/plugins/src/base/rules/reference/leaf-only-lifecycle.md +1 -1
  356. package/plugins/src/base/rules/reference/observability-audit.md +1 -1
  357. package/plugins/src/base/rules/reference/prd-definition-of-ready.md +1 -1
  358. package/plugins/src/base/rules/reference/prd-lifecycle-rollup.md +1 -1
  359. package/plugins/src/base/rules/reference/pre-flight-autofill.md +1 -1
  360. package/plugins/src/base/rules/reference/promotion-contract.md +1 -1
  361. package/plugins/src/base/rules/reference/readiness-rubric.md +1 -1
  362. package/plugins/src/base/rules/reference/ready-role-filing.md +37 -1
  363. package/plugins/src/base/rules/reference/rejection-detection.md +1 -1
  364. package/plugins/src/base/rules/reference/repo-scope-split.md +9 -10
  365. package/plugins/src/base/rules/reference/reset-seed-coverage.md +1 -1
  366. package/plugins/src/base/rules/reference/usage-accounting.md +1 -1
  367. package/plugins/src/base/rules/reference/work-item-definition-of-ready.md +1 -1
  368. package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +85 -17
  369. package/plugins/src/base/scripts/qa-signal-lifecycle.mjs +5 -2
  370. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +38 -15
  371. package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +15 -1
  372. package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +37 -13
  373. package/plugins/src/base/skills/lisa-jira-write-ticket/SKILL.md +15 -1
  374. package/plugins/src/base/skills/lisa-linear-access/SKILL.md +56 -7
  375. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +36 -11
  376. package/plugins/src/base/skills/lisa-linear-prd-intake/SKILL.md +12 -2
  377. package/plugins/src/base/skills/lisa-linear-validate-issue/SKILL.md +1 -18
  378. package/plugins/src/base/skills/lisa-linear-verify/SKILL.md +0 -17
  379. package/plugins/src/base/skills/lisa-linear-write-issue/SKILL.md +18 -35
  380. package/plugins/src/base/skills/lisa-prd-ticket-coverage/SKILL.md +2 -2
  381. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +66 -12
  382. package/plugins/src/base/skills/lisa-rework-triage/SKILL.md +2 -1
  383. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +2 -1
  384. package/plugins/src/base/skills/lisa-setup-linear/SKILL.md +1 -1
  385. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +7 -2
  386. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +11 -4
  387. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +17 -5
  388. package/plugins/src/base/skills/lisa-track/SKILL.md +32 -4
  389. package/plugins/src/base/skills/lisa-tracker-build-intake/SKILL.md +15 -1
  390. package/scripts/build-plugins.sh +4 -0
  391. package/scripts/copy-opencode-plugin-templates.mjs +5 -1
  392. package/scripts/lisa-enforcement-fallback.sh +9 -1
  393. package/scripts/two-channel-couplings.json +5 -33
  394. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +551 -13
  395. package/typescript/create-only/.github/required-checks.json +1 -1
@@ -15,6 +15,11 @@
15
15
  * node scripts/check-skipped-required-checks.mjs [rootDir] [--json]
16
16
  * node scripts/check-skipped-required-checks.mjs --vacuity [--fail-on-vacuous]
17
17
  * node scripts/check-skipped-required-checks.mjs --pr=1234 [--repo=OWNER/NAME]
18
+ * node scripts/check-skipped-required-checks.mjs --outcomes (reads LISA_JOB_RESULTS, LISA_JOB_NAMES, LISA_GATE_MOMENT)
19
+ *
20
+ * `--outcomes` is the arm the `🔒 Skipped Required Checks` job rests its verdict
21
+ * on. Without it, this reports only the vestigial `skip_jobs` token arm, which
22
+ * is not a verdict about whether a required context skipped.
18
23
  *
19
24
  * `--vacuity` is the WIRED form of the third bullet: it resolves the pull
20
25
  * request itself (`--pr`, else the Actions event payload, else `GITHUB_REF`,
@@ -31,13 +36,79 @@
31
36
  *
32
37
  * Two of the three live here:
33
38
  *
34
- * - **Skipped** (the offline arm, below): GitHub counts a `skipped`
35
- * required check as SATISFIED, so a `skip_jobs` token makes the gate
36
- * decorative. Static, offline, BLOCKING.
39
+ * - **Skipped** (`--outcomes` arm): GitHub counts a `skipped` required check
40
+ * as SATISFIED. The `🔒 Skipped Required Checks` job hands this arm every
41
+ * job result of its own workflow run, and it FAILS when a ruleset-required
42
+ * context's job concluded `skipped`. Offline, BLOCKING in every
43
+ * enforcement mode.
37
44
  * - **Vacuous** (`--pr` arm): the check really ran and really reported
38
45
  * `success`, having done no work — measured on CodeRabbit posting
39
46
  * `success — "Review rate limited"`. Live, per-PR, REPORTING ONLY.
40
47
  *
48
+ * ## `--outcomes` — why the verdict rests on OUTCOMES, and the token arm is VESTIGIAL
49
+ *
50
+ * This guard used to answer "can a skip silence a required check?" by reading
51
+ * `skip_jobs` TOKENS out of the caller's workflow and comparing what each was
52
+ * declared to silence against `required_contexts`. `skip_jobs` was then retired
53
+ * in favour of gate levels in `.lisa.config.json`, which skip a job through the
54
+ * gate plan and leave no token behind. The token arm kept running and kept
55
+ * printing `✅ 0 skip_jobs token(s) examined; none silences a ruleset-required
56
+ * status check`. MEASURED on a caller repository in the portfolio: a required
57
+ * `🧾 BDD Behavior Contract` context concluded `skipped` on seven of its sixty
58
+ * most recent merges, and this job was green on every one. It was green on
59
+ * exactly the condition it was built to refuse, because it inspected a
60
+ * declaration that had moved rather than the outcome the declaration was about.
61
+ *
62
+ * So the verdict now rests on the OUTCOME — what each required context's job
63
+ * actually concluded in this run. That is mechanism-independent: it holds
64
+ * whether the skip came from a gate level, a token, or a hand-written `if:`, so
65
+ * it survives the next replacement of whatever silences checks. A guard that
66
+ * inspects a declaration is correct only until the declaration moves.
67
+ *
68
+ * The token arm is RETAINED because a caller may still pass `skip_jobs`, and an
69
+ * undeclared or spaced token is still worth naming. It is VESTIGIAL: its
70
+ * findings can only ADD failures, and its clean result prints a count — never a
71
+ * ✅, never a claim that nothing was silenced. On its own (no `--outcomes`) it
72
+ * is not a verdict about skipped required contexts, and its report says so.
73
+ *
74
+ * How the outcome arm sees results, and what it deliberately does not claim:
75
+ *
76
+ * - **No token.** The job `needs:` every other job in `quality.yml` with
77
+ * `if: always()`, so it starts once every sibling has concluded, and reads
78
+ * their results from `toJSON(needs)`. Reading check runs through the API
79
+ * would need `checks: read`, which callers do not grant and a called
80
+ * workflow cannot request — and #3599's reduction to offline arms holds.
81
+ * - **Names on the ` / ` boundary, and a prefix that says whose they are.**
82
+ * `needs` carries results but no names, so the workflow carries a job id →
83
+ * display name map beside it (a test derives it from the jobs). A job in a
84
+ * called workflow posts `<caller job> / <job name>`, and the CALLER's job
85
+ * name is not visible from inside, so a context matches a job by equality or
86
+ * by a suffix bounded at ` / ` — see {@link producesContext}. A NAME MATCH IS
87
+ * NOT OWNERSHIP: `Some Other Workflow / 🧹 Lint` matches the local `🧹 Lint`
88
+ * while being posted by a workflow this run cannot see, and reporting the
89
+ * local result for it would be a false green. So a context is judged only
90
+ * under the ONE prefix this run posts — declared as
91
+ * `ruleset.context_prefix`, else inferred by majority — and every other
92
+ * prefix is reported NOT EXAMINED. See {@link owningPrefix}.
93
+ * - **Only at `pull-request`.** Other moments skip most jobs by design and gate
94
+ * no merge; see {@link MERGE_GATE_MOMENT}.
95
+ * - **Only what this run posts.** An external app, another workflow, or a
96
+ * matrix leg named at runtime posts a context no job here can speak for. It
97
+ * is listed as NOT EXAMINED, never silently counted — GitHub records that
98
+ * outcome, and this run cannot see it.
99
+ * - **Refusals FAIL, in every mode.** Zero required contexts examined, an
100
+ * untranscribed snapshot, or results that cannot be read each fail. "Examined
101
+ * nothing" must never render like "found nothing" (see `OUTCOME_REFUSALS`).
102
+ *
103
+ * The job checks this prover out from the WORKFLOW's own repository at the
104
+ * workflow's own commit (`job.workflow_repository@job.workflow_sha`), never
105
+ * from the caller's `scripts/`. `quality.yml` is consumed at `@main`; `scripts/`
106
+ * arrives by `lisa apply` at whatever version the caller pinned. A caller's copy
107
+ * predating this arm ignores `--outcomes` and prints the vestigial token line as
108
+ * though it were a verdict — the false green this arm replaces. The workflow
109
+ * defines the arguments, so the code reading them must come from the same
110
+ * revision.
111
+ *
41
112
  * ## Where this runs
42
113
  *
43
114
  * Lisa's `quality.yml` runs this on every pull request. EVERY arm is offline:
@@ -347,6 +418,9 @@ export const VIOLATIONS = Object.freeze({
347
418
  reviewCarried: "review_evidence_carried_unreviewed",
348
419
  reviewObjected: "review_evidence_objected",
349
420
  reviewObjectionUnread: "review_evidence_objection_unread",
421
+ requiredSkipped: "required_context_skipped",
422
+ requiredNotRun: "required_context_not_run",
423
+ requiredAmbiguous: "required_context_ambiguous",
350
424
  });
351
425
 
352
426
  /**
@@ -454,8 +528,20 @@ const ENFORCEMENT_MODES = Object.freeze(["error", "warn"]);
454
528
  * both that a context is ruleset-required AND that a token it actually skips
455
529
  * silences it; that is a reviewed state, and shipping past it is the exact
456
530
  * defect this file exists to refuse.
531
+ *
532
+ * The three outcome kinds block for a stronger reason still: they are not a
533
+ * declaration at all but an OBSERVATION. A required context whose job concluded
534
+ * `skipped` in this very run is the false green itself, measured; a result that
535
+ * cannot be read, or a context two jobs could post, is a verdict this guard
536
+ * cannot render, and rendering it green anyway is the collapse the outcome arm
537
+ * was built to stop.
457
538
  */
458
- const ALWAYS_BLOCKING = Object.freeze([VIOLATIONS.suppressesRequired]);
539
+ const ALWAYS_BLOCKING = Object.freeze([
540
+ VIOLATIONS.suppressesRequired,
541
+ VIOLATIONS.requiredSkipped,
542
+ VIOLATIONS.requiredNotRun,
543
+ VIOLATIONS.requiredAmbiguous,
544
+ ]);
459
545
 
460
546
  /**
461
547
  * Violation kinds that NEVER fail the build, in any enforcement mode.
@@ -3946,16 +4032,350 @@ export function inspectVacuity(argv, declaration, options = {}) {
3946
4032
  };
3947
4033
  }
3948
4034
 
4035
+ /**
4036
+ * Selects the OUTCOME arm — the one the `🔒 Skipped Required Checks` job rests
4037
+ * its verdict on. See the `--outcomes` section of this file's header.
4038
+ */
4039
+ export const OUTCOMES_FLAG = "--outcomes";
4040
+
4041
+ /**
4042
+ * The environment the outcome arm reads, exactly as `quality.yml` sets it.
4043
+ *
4044
+ * Environment rather than flags because two of the three are JSON built from
4045
+ * expressions, and `toJSON(needs)` carries job OUTPUTS. An expression spliced
4046
+ * into a `run:` line is a shell-injection surface; placed in `env:` it is inert
4047
+ * data.
4048
+ *
4049
+ * - `results` — `toJSON(needs)`: each sibling job's final `result`.
4050
+ * - `names` — job id → the display name GitHub composes into the context.
4051
+ * - `moment` — the workflow's `moment` input.
4052
+ */
4053
+ export const OUTCOME_ENV = Object.freeze({
4054
+ results: "LISA_JOB_RESULTS",
4055
+ names: "LISA_JOB_NAMES",
4056
+ moment: "LISA_GATE_MOMENT",
4057
+ });
4058
+
4059
+ /**
4060
+ * The one moment whose run is the merge gate a ruleset's required checks read.
4061
+ *
4062
+ * Every other moment skips most jobs BY DESIGN — a `continuous:dev` caller wants
4063
+ * its E2E gates and nothing else — and gates no merge. Judging those runs would
4064
+ * redden every nightly over a skip no pull request can merge on, and a gate that
4065
+ * is red every night gets deleted rather than read.
4066
+ */
4067
+ export const MERGE_GATE_MOMENT = "pull-request";
4068
+
4069
+ /**
4070
+ * Job results that show the job was scheduled to run.
4071
+ *
4072
+ * `failure` and `cancelled` are here rather than in a finding: GitHub does NOT
4073
+ * count either as satisfying a required check, so the context already blocks
4074
+ * the merge by itself. This arm exists for the one result GitHub DOES count as
4075
+ * satisfied while proving nothing.
4076
+ */
4077
+ const OUTCOMES_THAT_RAN = Object.freeze(["success", "failure", "cancelled"]);
4078
+
4079
+ /**
4080
+ * Why the outcome arm rendered no verdict, as stable tokens.
4081
+ *
4082
+ * Each one FAILS, in every enforcement mode. `warn` exists so a fresh install
4083
+ * does not redden on hygiene findings; it is not a licence to print "examined
4084
+ * nothing" in the shape of "found nothing", because that collapse is how this
4085
+ * job came to be green over a skipped required context.
4086
+ */
4087
+ export const OUTCOME_REFUSALS = Object.freeze({
4088
+ untrusted: "outcomes_snapshot_untrusted",
4089
+ unreadable: "outcomes_results_unreadable",
4090
+ examinedNothing: "outcomes_examined_nothing",
4091
+ });
4092
+
4093
+ /**
4094
+ * Whether a job with this display name posts this required context.
4095
+ *
4096
+ * A job in a called workflow posts `<caller job name> / <job name>`, and the
4097
+ * caller's job name is not visible from inside the called workflow without a
4098
+ * token. So the match is exact equality OR a suffix bounded by the ` / `
4099
+ * separator: `🧹 Lint` matches `🔍 Quality Checks / 🧹 Lint` and never
4100
+ * `🔍 Quality Checks / 🐢 Slow Lint Rules`, and a bare `Lint` never matches
4101
+ * `Slow Lint`. Two job names that could both match one context are refused as
4102
+ * ambiguous by {@link evaluateRequiredOutcomes} rather than resolved by guessing.
4103
+ *
4104
+ * NAME MATCHING ALONE IS NOT OWNERSHIP, and this function is deliberately only
4105
+ * half the rule — see {@link owningPrefix}. `Some Other Workflow / 🧹 Lint`
4106
+ * satisfies this test against the local `🧹 Lint` job while being posted by a
4107
+ * workflow this run cannot see, and reporting the local result for it is a
4108
+ * FALSE GREEN of exactly the kind this file exists to refuse.
4109
+ *
4110
+ * @param {string} jobName - The job's display name
4111
+ * @param {string} context - A required context, verbatim
4112
+ * @returns {boolean} True when the job's NAME matches that context
4113
+ */
4114
+ export function producesContext(jobName, context) {
4115
+ return context === jobName || context.endsWith(` / ${jobName}`);
4116
+ }
4117
+
4118
+ /**
4119
+ * The prefix of a context matched by name, or `""` for an unprefixed match.
4120
+ *
4121
+ * @param {string} context - A required context
4122
+ * @param {string} jobName - The job name that matched it
4123
+ * @returns {string} The caller-job prefix the context carries
4124
+ */
4125
+ function prefixOf(context, jobName) {
4126
+ return context === jobName
4127
+ ? ""
4128
+ : context.slice(0, context.length - jobName.length - " / ".length);
4129
+ }
4130
+
4131
+ /**
4132
+ * Which caller-job prefix this run OWNS, and how that was decided.
4133
+ *
4134
+ * The problem it solves: every context this run posts carries one prefix — the
4135
+ * caller's job name — and a context carrying any OTHER prefix belongs to a
4136
+ * workflow this run cannot see, even when its trailing segment happens to equal
4137
+ * a local job name. Matching on the name alone would report a local job's result
4138
+ * for somebody else's check.
4139
+ *
4140
+ * Two ways to know the prefix, in order:
4141
+ *
4142
+ * 1. **Declared.** `ruleset.context_prefix` in the per-repo declaration, when
4143
+ * present, is the caller's job name as transcribed by whoever transcribed
4144
+ * the contexts. Exact, and nothing is inferred.
4145
+ * 2. **Inferred, by majority.** This workflow posts a context for every gate —
4146
+ * dozens — while another workflow contributes one or two. So the prefix
4147
+ * accounting for the MOST required contexts is this run's. A tie is not
4148
+ * resolved: with no majority there is no evidence, and every matched context
4149
+ * is reported NOT EXAMINED rather than attributed by coin-toss.
4150
+ *
4151
+ * The failure direction is what makes the inference acceptable: a context whose
4152
+ * prefix is not the chosen one is never examined and never claimed, so being
4153
+ * wrong costs coverage that is reported as missing — never a green over a check
4154
+ * nobody read.
4155
+ *
4156
+ * @param {object} declaration - The per-repo declaration
4157
+ * @param {ReadonlyArray<{context: string, prefix: string}>} matches - Name matches
4158
+ * @returns {{prefix: string|null, source: string}} The owning prefix, and why
4159
+ */
4160
+ export function owningPrefix(declaration, matches) {
4161
+ const declared = declaration.ruleset?.context_prefix;
4162
+ if (typeof declared === "string" && declared.trim() !== "") {
4163
+ return { prefix: declared, source: "declared in `ruleset.context_prefix`" };
4164
+ }
4165
+ const counts = new Map();
4166
+ for (const { prefix } of matches) {
4167
+ counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
4168
+ }
4169
+ if (counts.size === 0) return { prefix: null, source: "no context matched" };
4170
+ const ranked = [...counts.entries()].sort(
4171
+ (left, right) => right[1] - left[1]
4172
+ );
4173
+ const [[prefix, top]] = ranked;
4174
+ if (ranked.length > 1 && ranked[1][1] === top) {
4175
+ return {
4176
+ prefix: null,
4177
+ source: `two prefixes match ${top} context(s) each, so which one this run posts is not evidenced`,
4178
+ };
4179
+ }
4180
+ return {
4181
+ prefix,
4182
+ source: `inferred from ${top} of ${matches.length} matched context(s)`,
4183
+ };
4184
+ }
4185
+
4186
+ /**
4187
+ * Judges each required context by what its job DID in this run.
4188
+ *
4189
+ * Pure: the whole verdict is a function of the snapshot, the results and the
4190
+ * names, so every branch is testable without a workflow run.
4191
+ *
4192
+ * @param {object} declaration - The per-repo declaration
4193
+ * @param {Record<string, {result?: string}>} results - `toJSON(needs)`, parsed
4194
+ * @param {Record<string, string>} names - Job id → display name
4195
+ * @returns {{examined: {context: string, job: string, result: string}[], notExamined: string[], violations: object[], prefix: {prefix: string|null, source: string}}} What was judged, what could not be seen, the findings, and whose contexts it judged
4196
+ */
4197
+ export function evaluateRequiredOutcomes(declaration, results, names) {
4198
+ const required = Array.isArray(declaration.required_contexts)
4199
+ ? declaration.required_contexts
4200
+ : [];
4201
+ // TWO PASSES, and the first one is the ownership rule. A name match says a
4202
+ // context COULD be this job's; only the prefix says it IS. See `owningPrefix`.
4203
+ const matched = required.flatMap(context => {
4204
+ const jobs = Object.keys(names).filter(job =>
4205
+ producesContext(names[job], context)
4206
+ );
4207
+ return jobs.length === 0
4208
+ ? []
4209
+ : [{ context, jobs, prefix: prefixOf(context, names[jobs[0]]) }];
4210
+ });
4211
+ const prefix = owningPrefix(declaration, matched);
4212
+ const owned = new Map(
4213
+ matched
4214
+ .filter(match => prefix.prefix !== null && match.prefix === prefix.prefix)
4215
+ .map(match => [match.context, match.jobs])
4216
+ );
4217
+
4218
+ const examined = [];
4219
+ const notExamined = [];
4220
+ const violations = [];
4221
+ for (const context of required) {
4222
+ const jobs = owned.get(context) ?? [];
4223
+ if (jobs.length === 0) {
4224
+ notExamined.push(context);
4225
+ continue;
4226
+ }
4227
+ if (jobs.length > 1) {
4228
+ violations.push({
4229
+ kind: VIOLATIONS.requiredAmbiguous,
4230
+ token: context,
4231
+ contexts: [context],
4232
+ message: `\`${context}\` is ruleset-required and could be posted by more than one job in this run (${jobs.map(job => `\`${job}\``).join(", ")}). Which outcome it carries cannot be decided without guessing, so it is refused rather than resolved. Rename a job so that no display name ends in \` / \` followed by another.`,
4233
+ });
4234
+ continue;
4235
+ }
4236
+ const [job] = jobs;
4237
+ const result = String(results[job]?.result ?? "");
4238
+ examined.push({ context, job, result });
4239
+ violations.push(...outcomeViolations(context, job, result));
4240
+ }
4241
+ return { examined, notExamined, violations, prefix };
4242
+ }
4243
+
4244
+ /**
4245
+ * Findings for one examined required context.
4246
+ *
4247
+ * @param {string} context - Required context
4248
+ * @param {string} job - The job that posts it
4249
+ * @param {string} result - That job's result in this run
4250
+ * @returns {object[]} Zero or one violation
4251
+ */
4252
+ function outcomeViolations(context, job, result) {
4253
+ if (result === "skipped") {
4254
+ return [
4255
+ {
4256
+ kind: VIOLATIONS.requiredSkipped,
4257
+ token: context,
4258
+ contexts: [context],
4259
+ message: `\`${context}\` is ruleset-required and its job \`${job}\` concluded \`skipped\`. GitHub counts a SKIPPED required status check as SATISFIED, so this pull request can merge on a check that ran zero steps. The mechanism does not change the outcome — a gate declared \`off\` at this moment, a \`skip_jobs\` token and a job condition all leave the same false green. Run the gate, or de-require the context in the ruleset and re-transcribe \`required_contexts\`.`,
4260
+ },
4261
+ ];
4262
+ }
4263
+ if (OUTCOMES_THAT_RAN.includes(result)) return [];
4264
+ return [
4265
+ {
4266
+ kind: VIOLATIONS.requiredNotRun,
4267
+ token: context,
4268
+ contexts: [context],
4269
+ message: `\`${context}\` is ruleset-required, but this run holds no readable result for its job \`${job}\` (read ${JSON.stringify(result)}). A result nobody can read is not evidence that the check ran, so it is refused rather than assumed. The job is most likely missing from the \`needs:\` of \`🔒 Skipped Required Checks\`.`,
4270
+ },
4271
+ ];
4272
+ }
4273
+
4274
+ /**
4275
+ * Parses the two JSON inputs, refusing anything that is not a plain object.
4276
+ *
4277
+ * @param {Record<string, string|undefined>} env - Environment
4278
+ * @returns {{results?: Record<string, {result?: string}>, names?: Record<string, string>, error?: string}} The parsed inputs, or why they could not be read
4279
+ */
4280
+ function readOutcomeEnv(env) {
4281
+ const parsed = {};
4282
+ for (const key of ["results", "names"]) {
4283
+ const variable = OUTCOME_ENV[key];
4284
+ let value;
4285
+ try {
4286
+ value = JSON.parse(String(env[variable] ?? ""));
4287
+ } catch {
4288
+ return {
4289
+ error: `\`${variable}\` is not JSON, so no job result could be read. The \`🔒 Skipped Required Checks\` job sets it; outside that job \`${OUTCOMES_FLAG}\` has nothing to judge.`,
4290
+ };
4291
+ }
4292
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
4293
+ return {
4294
+ error: `\`${variable}\` parsed to ${JSON.stringify(value)}, not an object keyed by job id.`,
4295
+ };
4296
+ }
4297
+ parsed[key] = value;
4298
+ }
4299
+ const names = Object.values(parsed.names);
4300
+ if (names.length === 0 || names.some(name => typeof name !== "string")) {
4301
+ return {
4302
+ error: `\`${OUTCOME_ENV.names}\` must map at least one job id to its display name, and every name must be a string.`,
4303
+ };
4304
+ }
4305
+ return parsed;
4306
+ }
4307
+
4308
+ /**
4309
+ * The OUTCOME arm: did each ruleset-required context this run posts actually run?
4310
+ *
4311
+ * @param {ReadonlyArray<string>} argv - CLI arguments
4312
+ * @param {object} declaration - The per-repo declaration
4313
+ * @param {{env?: Record<string, string|undefined>, trust?: {trusted: boolean, reason: string}}} [options] - Environment and snapshot trust
4314
+ * @returns {{moment: string, applicable: boolean, examined: object[], notExamined: string[], violations: object[], refusal: {kind: string, reason: string}|null}|undefined} The inspection, or undefined when `--outcomes` was not passed
4315
+ */
4316
+ export function inspectOutcomes(argv, declaration, options = {}) {
4317
+ if (!argv.includes(OUTCOMES_FLAG)) return undefined;
4318
+ const env = options.env ?? process.env;
4319
+ const moment = String(env[OUTCOME_ENV.moment] ?? "").trim();
4320
+ const base = {
4321
+ moment,
4322
+ applicable: true,
4323
+ examined: [],
4324
+ notExamined: [],
4325
+ violations: [],
4326
+ refusal: null,
4327
+ };
4328
+ if (moment !== "" && moment !== MERGE_GATE_MOMENT) {
4329
+ return { ...base, applicable: false };
4330
+ }
4331
+ /**
4332
+ * @param {string} kind - One of `OUTCOME_REFUSALS`
4333
+ * @param {string} reason - Why nothing was judged
4334
+ * @returns {object} The refused inspection
4335
+ */
4336
+ const refuse = (kind, reason) => ({ ...base, refusal: { kind, reason } });
4337
+ if (moment === "") {
4338
+ return refuse(
4339
+ OUTCOME_REFUSALS.unreadable,
4340
+ `\`${OUTCOME_ENV.moment}\` is empty, so this cannot tell whether this run is the \`${MERGE_GATE_MOMENT}\` run a merge is gated on. Refused rather than assumed either way.`
4341
+ );
4342
+ }
4343
+ if (options.trust?.trusted === false) {
4344
+ return refuse(
4345
+ OUTCOME_REFUSALS.untrusted,
4346
+ "`required_contexts` has never been transcribed from a live ruleset (see NOT CHECKED above), so which job results a merge depends on is unknown. Examining no required context is not the same as finding none skipped."
4347
+ );
4348
+ }
4349
+ const read = readOutcomeEnv(env);
4350
+ if (read.error !== undefined) {
4351
+ return refuse(OUTCOME_REFUSALS.unreadable, read.error);
4352
+ }
4353
+ const evaluated = evaluateRequiredOutcomes(
4354
+ declaration,
4355
+ read.results,
4356
+ read.names
4357
+ );
4358
+ const refusal =
4359
+ evaluated.examined.length > 0
4360
+ ? null
4361
+ : {
4362
+ kind: OUTCOME_REFUSALS.examinedNothing,
4363
+ reason: `Not one of the ${evaluated.notExamined.length} context(s) in \`required_contexts\` was judged, so no outcome was examined, and a guard that examined nothing must not render like one that found nothing. Either no context this workflow posts is in the snapshot — transcribe the real ruleset — or the contexts were renamed, or this run does not own their prefix (${evaluated.prefix.source}). Names are matched byte for byte on the \` / \` boundary, and a context is judged only under the prefix this run posts; \`ruleset.context_prefix\` states that prefix outright.`,
4364
+ };
4365
+ return { ...base, ...evaluated, refusal };
4366
+ }
4367
+
3949
4368
  /**
3950
4369
  * Runs the guard.
3951
4370
  *
3952
4371
  * Every arm is OFFLINE. There is no network read and no token anywhere in this
3953
4372
  * path: the required-context rules answer from the committed snapshot when
3954
- * {@link snapshotTrust} believes it, and refuse when it does not.
4373
+ * {@link snapshotTrust} believes it, and refuse when it does not. The outcome
4374
+ * arm reads job results the workflow hands it through the environment.
3955
4375
  *
3956
4376
  * @param {ReadonlyArray<string>} argv - CLI arguments
3957
- * @param {object} [options] - Injection seams forwarded to {@link inspectVacuity}
3958
- * @returns {{violations: object[], checked: number, tokens: string[], enforcement: string, trust: {trusted: boolean, reason: string}, recipe: string, pr: string|undefined, evidenceChecked: number, vacuity: object|undefined}} The result
4377
+ * @param {object} [options] - Injection seams forwarded to {@link inspectVacuity}; `env` also reaches {@link inspectOutcomes}
4378
+ * @returns {{violations: object[], checked: number, tokens: string[], enforcement: string, trust: {trusted: boolean, reason: string}, recipe: string, pr: string|undefined, evidenceChecked: number, vacuity: object|undefined, outcomes: object|undefined}} The result
3959
4379
  */
3960
4380
  export function runGuard(argv, options = {}) {
3961
4381
  if (argv.includes(RETIRED_REMOTE_FLAG)) {
@@ -3991,6 +4411,16 @@ export function runGuard(argv, options = {}) {
3991
4411
  });
3992
4412
  if (vacuity !== undefined) violations.push(...vacuity.violations);
3993
4413
 
4414
+ // The OUTCOME arm, which the CI job's verdict rests on. Its findings are
4415
+ // ALWAYS_BLOCKING and its refusal fails in every mode, so nothing the
4416
+ // vestigial token arm above reports — clean or otherwise — can turn its red
4417
+ // into a pass.
4418
+ const outcomes = inspectOutcomes(argv, declaration, {
4419
+ env: options.env,
4420
+ trust,
4421
+ });
4422
+ if (outcomes !== undefined) violations.push(...outcomes.violations);
4423
+
3994
4424
  return {
3995
4425
  violations,
3996
4426
  checked: result.checked,
@@ -4003,6 +4433,7 @@ export function runGuard(argv, options = {}) {
4003
4433
  verdict: vacuity?.verdict,
4004
4434
  waiveRate: vacuity?.waiveRate,
4005
4435
  vacuity,
4436
+ outcomes,
4006
4437
  };
4007
4438
  }
4008
4439
 
@@ -4120,7 +4551,7 @@ export function violationBlocks(violation, policy) {
4120
4551
  *
4121
4552
  * @param {object} result - Guard result
4122
4553
  * @param {object} policy - Active CLI policy
4123
- * @returns {{blocking: object[], refusal: object|null, refusalBlocks: boolean, failed: boolean}} CLI outcome
4554
+ * @returns {{blocking: object[], refusal: object|null, refusalBlocks: boolean, outcomesRefusal: object|null, failed: boolean}} CLI outcome
4124
4555
  */
4125
4556
  function cliOutcome(result, policy) {
4126
4557
  const blocking = result.violations.filter(violation =>
@@ -4129,11 +4560,14 @@ function cliOutcome(result, policy) {
4129
4560
  const refusal = result.vacuity?.refusal ?? null;
4130
4561
  const refusalBlocks =
4131
4562
  refusal !== null && (!policy.warnOnly || policy.requireReviewEvidence);
4563
+ // Unconditional: see OUTCOME_REFUSALS for why `warn` cannot downgrade it.
4564
+ const outcomesRefusal = result.outcomes?.refusal ?? null;
4132
4565
  const failed =
4133
4566
  blocking.length > 0 ||
4134
4567
  (!result.trust.trusted && !policy.warnOnly) ||
4135
- refusalBlocks;
4136
- return { blocking, refusal, refusalBlocks, failed };
4568
+ refusalBlocks ||
4569
+ outcomesRefusal !== null;
4570
+ return { blocking, refusal, refusalBlocks, outcomesRefusal, failed };
4137
4571
  }
4138
4572
 
4139
4573
  /**
@@ -4202,7 +4636,7 @@ function appendSnapshotRefusal(lines, result, policy) {
4202
4636
  ""
4203
4637
  );
4204
4638
  process.stderr.write(
4205
- `::${policy.warnOnly ? "warning" : "error"} title=Skipped-required checks NOT CHECKED::${result.trust.reason.split("\n")[0]}\n`
4639
+ `::${policy.warnOnly && result.outcomes?.applicable !== true ? "warning" : "error"} title=Skipped-required checks NOT CHECKED::${result.trust.reason.split("\n")[0]}\n`
4206
4640
  );
4207
4641
  }
4208
4642
 
@@ -4243,8 +4677,12 @@ function appendCleanResult(lines, result, refusal) {
4243
4677
  return;
4244
4678
  }
4245
4679
 
4680
+ // VESTIGIAL, and worded so it cannot be read as a verdict. This line used to
4681
+ // be `✅ … none silences a ruleset-required status check`, and it printed
4682
+ // exactly that, over zero tokens, on a pull request that merged with a
4683
+ // required context skipped. The count is real; the count is all it may claim.
4246
4684
  lines.push(
4247
- `✅ ${result.checked} \`skip_jobs\` token(s) examined; none silences a ruleset-required status check.`
4685
+ `ℹ️ ${result.checked} \`skip_jobs\` token(s) examined by the VESTIGIAL declaration arm. \`skip_jobs\` is retired, so this is NOT a verdict that no required context was skipped — only the outcome arm (\`${OUTCOMES_FLAG}\`, run by the \`🔒 Skipped Required Checks\` job) observes that.`
4248
4686
  );
4249
4687
  if (result.pr === undefined) return;
4250
4688
  const settleNote =
@@ -4256,6 +4694,18 @@ function appendCleanResult(lines, result, refusal) {
4256
4694
  );
4257
4695
  }
4258
4696
 
4697
+ /**
4698
+ * What a finding count was counted across, naming both arms when both ran.
4699
+ *
4700
+ * @param {object} result - Guard result
4701
+ * @returns {string} The scope phrase
4702
+ */
4703
+ function findingScope(result) {
4704
+ const tokens = `${result.checked} \`skip_jobs\` token(s)`;
4705
+ if (result.outcomes?.applicable !== true) return tokens;
4706
+ return `${result.outcomes.examined.length} required-context outcome(s) and ${tokens}`;
4707
+ }
4708
+
4259
4709
  /**
4260
4710
  * Appends annotations and prose for every finding.
4261
4711
  *
@@ -4267,7 +4717,7 @@ function appendCleanResult(lines, result, refusal) {
4267
4717
  */
4268
4718
  function appendFindingReport(lines, result, policy, blocking) {
4269
4719
  lines.push(
4270
- `${blocking.length > 0 ? "❌" : "⚠️"} ${result.violations.length} violation(s) across ${result.checked} \`skip_jobs\` token(s):`,
4720
+ `${blocking.length > 0 ? "❌" : "⚠️"} ${result.violations.length} violation(s) across ${findingScope(result)}:`,
4271
4721
  ""
4272
4722
  );
4273
4723
  for (const violation of result.violations) {
@@ -4347,6 +4797,93 @@ function appendReviewWaiverGuidance(lines) {
4347
4797
  );
4348
4798
  }
4349
4799
 
4800
+ /**
4801
+ * Appends what the outcome arm examined, what it could not see, and why it
4802
+ * refused — so "examined and found nothing" never reads like "examined nothing".
4803
+ *
4804
+ * @param {string[]} lines - Report lines
4805
+ * @param {object|undefined} outcomes - The outcome inspection
4806
+ * @returns {void}
4807
+ */
4808
+ function appendOutcomeReport(lines, outcomes) {
4809
+ if (outcomes === undefined) return;
4810
+ if (!outcomes.applicable) {
4811
+ lines.push(
4812
+ `ℹ️ **Outcome arm not applicable** — this run's gate moment is \`${outcomes.moment}\`. A ruleset's required checks gate a merge from the \`${MERGE_GATE_MOMENT}\` run; this run gates none, so NOT ONE required context was examined here, and nothing in this report is a verdict about a pull request.`,
4813
+ ""
4814
+ );
4815
+ return;
4816
+ }
4817
+ appendOutcomeRefusal(lines, outcomes.refusal);
4818
+ appendExaminedOutcomes(lines, outcomes);
4819
+ appendUnexaminedContexts(lines, outcomes.notExamined);
4820
+ }
4821
+
4822
+ /**
4823
+ * Appends and annotates an outcome inspection that judged nothing.
4824
+ *
4825
+ * @param {string[]} lines - Report lines
4826
+ * @param {{kind: string, reason: string}|null} refusal - The refusal, if any
4827
+ * @returns {void}
4828
+ */
4829
+ function appendOutcomeRefusal(lines, refusal) {
4830
+ if (refusal === null) return;
4831
+ lines.push(
4832
+ `⛔ **NOT EXAMINED** (\`${refusal.kind}\`) — the outcome arm judged no required context, and will not report that none was skipped. This FAILS in every enforcement mode.`,
4833
+ "",
4834
+ refusal.reason,
4835
+ ""
4836
+ );
4837
+ process.stderr.write(
4838
+ `::error title=${refusal.kind}::${refusal.reason.split("\n")[0]}\n`
4839
+ );
4840
+ }
4841
+
4842
+ /**
4843
+ * Appends every examined required context with the result its job reported.
4844
+ *
4845
+ * @param {string[]} lines - Report lines
4846
+ * @param {{examined: {context: string, job: string, result: string}[], violations: object[]}} outcomes - The outcome inspection
4847
+ * @returns {void}
4848
+ */
4849
+ function appendExaminedOutcomes(lines, outcomes) {
4850
+ if (outcomes.examined.length === 0) return;
4851
+ const count = outcomes.examined.length;
4852
+ const findings = outcomes.violations.length;
4853
+ const owned =
4854
+ outcomes.prefix?.prefix === undefined || outcomes.prefix?.prefix === null
4855
+ ? ""
4856
+ : ` Contexts judged are those this run posts, under the prefix ${JSON.stringify(outcomes.prefix.prefix)} (${outcomes.prefix.source}); any other prefix belongs to a workflow this run cannot see.`;
4857
+ lines.push(
4858
+ findings === 0
4859
+ ? `✅ ${count} ruleset-required context(s) examined against this run's job results; none was skipped.${owned}`
4860
+ : `❌ ${count} ruleset-required context(s) examined against this run's job results; ${findings} did not prove it ran (see the violations below).${owned}`,
4861
+ "",
4862
+ ...outcomes.examined.map(
4863
+ ({ context, job, result }) =>
4864
+ `- \`${context}\` ← job \`${job}\`: \`${result || "(no result)"}\``
4865
+ ),
4866
+ ""
4867
+ );
4868
+ }
4869
+
4870
+ /**
4871
+ * Appends the required contexts no job in this run posts.
4872
+ *
4873
+ * @param {string[]} lines - Report lines
4874
+ * @param {ReadonlyArray<string>} notExamined - Contexts this run cannot see
4875
+ * @returns {void}
4876
+ */
4877
+ function appendUnexaminedContexts(lines, notExamined) {
4878
+ if (notExamined.length === 0) return;
4879
+ lines.push(
4880
+ `Not examined — ${notExamined.length} required context(s) this run does not post: an external app, another workflow (including one whose job name matches a local one but whose prefix says it is not this run's), or a matrix leg named at runtime. GitHub records their outcomes; this run cannot see them and makes no claim about them.`,
4881
+ "",
4882
+ ...notExamined.map(context => `- \`${context}\``),
4883
+ ""
4884
+ );
4885
+ }
4886
+
4350
4887
  /**
4351
4888
  * Builds the complete human-readable report.
4352
4889
  *
@@ -4360,6 +4897,7 @@ function buildTextReport(result, policy, outcome) {
4360
4897
  appendVerdictReport(lines, result);
4361
4898
  appendSnapshotRefusal(lines, result, policy);
4362
4899
  appendInspectionRefusal(lines, outcome);
4900
+ appendOutcomeReport(lines, result.outcomes);
4363
4901
  if (result.violations.length === 0) {
4364
4902
  appendCleanResult(lines, result, outcome.refusal);
4365
4903
  } else {