@codyswann/lisa 4.49.0 → 4.50.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 (409) hide show
  1. package/all/copy-overwrite/scripts/check-conflict-markers.mjs +4 -1
  2. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +163 -0
  3. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +178 -1
  4. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +89 -10
  5. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +44 -0
  6. package/all/copy-overwrite/scripts/lisa-lint-staged-preflight.mjs +1 -1
  7. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +24 -24
  8. package/all/copy-overwrite/scripts/lisa-work-item.mjs +490 -81
  9. package/cdk/package-lisa/package.lisa.json +45 -15
  10. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  11. package/dist/core/lisa-owned-hash-ledger.js +76 -0
  12. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  13. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  14. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  15. package/dist/core/upstream-evidence-manifest.js +120 -227
  16. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  17. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +45 -1
  18. package/dist/opencode/plugin-templates/parity-safety-net.sh +44 -0
  19. package/expo/package-lisa/package.lisa.json +83 -53
  20. package/harper-fabric/package-lisa/package.lisa.json +62 -32
  21. package/nestjs/package-lisa/package.lisa.json +57 -27
  22. package/npm-package/package-lisa/package.lisa.json +43 -7
  23. package/package.json +11 -7
  24. package/phaser/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
  25. package/phaser/package-lisa/package.lisa.json +62 -32
  26. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  29. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +4 -0
  30. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +4 -0
  31. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +4 -0
  32. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +4 -1
  33. package/plugins/lisa/hooks/block-direct-issue-create.sh +89 -10
  34. package/plugins/lisa/hooks/discharge-work-item-gates.sh +7 -1
  35. package/plugins/lisa/hooks/parity-safety-net.sh +44 -0
  36. package/plugins/lisa/hooks/threshold-ratchet-families.mjs +12 -0
  37. package/plugins/lisa/rules/eager/00-rule-index.md +79 -0
  38. package/plugins/lisa/rules/eager/base-rules.md +10 -0
  39. package/plugins/lisa/rules/eager/config-resolution.md +1 -88
  40. package/plugins/lisa/rules/reference/automation-runbook-contract.md +97 -0
  41. package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +56 -0
  42. package/plugins/lisa/rules/reference/blocker-containment.md +58 -0
  43. package/plugins/lisa/rules/reference/claim-archaeology.md +42 -0
  44. package/plugins/lisa/rules/reference/claim-evidence-mapping.md +83 -0
  45. package/plugins/lisa/rules/reference/claim-time-guards.md +37 -0
  46. package/plugins/lisa/rules/reference/config-resolution.md +97 -0
  47. package/plugins/lisa/rules/reference/control-reachability.md +44 -0
  48. package/plugins/lisa/rules/reference/convergent-review.md +31 -0
  49. package/plugins/lisa/rules/reference/credential-substrate-precedence.md +57 -0
  50. package/plugins/lisa/rules/reference/dependency-decision-records.md +37 -0
  51. package/plugins/lisa/rules/reference/dependency-internalization-kit.md +47 -0
  52. package/plugins/lisa/rules/reference/dependency-trust-classes.md +48 -0
  53. package/plugins/lisa/rules/reference/deployed-state-readback.md +52 -0
  54. package/plugins/lisa/rules/reference/derived-branch-plan.md +52 -0
  55. package/plugins/lisa/rules/reference/design-source-of-truth.md +43 -0
  56. package/plugins/lisa/rules/reference/design-value-binding.md +103 -0
  57. package/plugins/lisa/rules/reference/documentation-source-paths.md +18 -0
  58. package/plugins/lisa/rules/reference/leaf-only-lifecycle.md +75 -0
  59. package/plugins/lisa/rules/reference/observability-audit.md +20 -0
  60. package/plugins/lisa/rules/reference/prd-definition-of-ready.md +37 -0
  61. package/plugins/lisa/rules/reference/prd-lifecycle-rollup.md +36 -0
  62. package/plugins/lisa/rules/reference/pre-flight-autofill.md +88 -0
  63. package/plugins/lisa/rules/reference/promotion-contract.md +21 -0
  64. package/plugins/lisa/rules/reference/readiness-rubric.md +63 -0
  65. package/plugins/lisa/rules/reference/ready-role-filing.md +34 -0
  66. package/plugins/lisa/rules/reference/rejection-detection.md +69 -0
  67. package/plugins/lisa/rules/reference/repo-scope-split.md +48 -0
  68. package/plugins/lisa/rules/reference/reset-seed-coverage.md +55 -0
  69. package/plugins/lisa/rules/reference/usage-accounting.md +38 -0
  70. package/plugins/lisa/rules/reference/work-item-definition-of-ready.md +32 -0
  71. package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +158 -1
  72. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  73. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +4 -0
  74. package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +4 -0
  75. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +4 -0
  76. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +4 -1
  77. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +89 -10
  78. package/plugins/lisa-agy/hooks/parity-safety-net.sh +44 -0
  79. package/plugins/lisa-agy/plugin.json +1 -1
  80. package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +158 -1
  81. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  82. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +4 -0
  83. package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +4 -0
  84. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +4 -0
  85. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +4 -1
  86. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  89. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +89 -10
  93. package/plugins/lisa-copilot/hooks/discharge-work-item-gates.sh +7 -1
  94. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +44 -0
  95. package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +12 -0
  96. package/plugins/lisa-copilot/rules/eager/00-rule-index.md +79 -0
  97. package/plugins/lisa-copilot/rules/eager/base-rules.md +10 -0
  98. package/plugins/lisa-copilot/rules/eager/config-resolution.md +1 -88
  99. package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +97 -0
  100. package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +56 -0
  101. package/plugins/lisa-copilot/rules/reference/blocker-containment.md +58 -0
  102. package/plugins/lisa-copilot/rules/reference/claim-archaeology.md +42 -0
  103. package/plugins/lisa-copilot/rules/reference/claim-evidence-mapping.md +83 -0
  104. package/plugins/lisa-copilot/rules/reference/claim-time-guards.md +37 -0
  105. package/plugins/lisa-copilot/rules/reference/config-resolution.md +97 -0
  106. package/plugins/lisa-copilot/rules/reference/control-reachability.md +44 -0
  107. package/plugins/lisa-copilot/rules/reference/convergent-review.md +31 -0
  108. package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +57 -0
  109. package/plugins/lisa-copilot/rules/reference/dependency-decision-records.md +37 -0
  110. package/plugins/lisa-copilot/rules/reference/dependency-internalization-kit.md +47 -0
  111. package/plugins/lisa-copilot/rules/reference/dependency-trust-classes.md +48 -0
  112. package/plugins/lisa-copilot/rules/reference/deployed-state-readback.md +52 -0
  113. package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +52 -0
  114. package/plugins/lisa-copilot/rules/reference/design-source-of-truth.md +43 -0
  115. package/plugins/lisa-copilot/rules/reference/design-value-binding.md +103 -0
  116. package/plugins/lisa-copilot/rules/reference/documentation-source-paths.md +18 -0
  117. package/plugins/lisa-copilot/rules/reference/leaf-only-lifecycle.md +75 -0
  118. package/plugins/lisa-copilot/rules/reference/observability-audit.md +20 -0
  119. package/plugins/lisa-copilot/rules/reference/prd-definition-of-ready.md +37 -0
  120. package/plugins/lisa-copilot/rules/reference/prd-lifecycle-rollup.md +36 -0
  121. package/plugins/lisa-copilot/rules/reference/pre-flight-autofill.md +88 -0
  122. package/plugins/lisa-copilot/rules/reference/promotion-contract.md +21 -0
  123. package/plugins/lisa-copilot/rules/reference/readiness-rubric.md +63 -0
  124. package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +34 -0
  125. package/plugins/lisa-copilot/rules/reference/rejection-detection.md +69 -0
  126. package/plugins/lisa-copilot/rules/reference/repo-scope-split.md +48 -0
  127. package/plugins/lisa-copilot/rules/reference/reset-seed-coverage.md +55 -0
  128. package/plugins/lisa-copilot/rules/reference/usage-accounting.md +38 -0
  129. package/plugins/lisa-copilot/rules/reference/work-item-definition-of-ready.md +32 -0
  130. package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +158 -1
  131. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  132. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +4 -0
  133. package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +4 -0
  134. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +4 -0
  135. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +4 -1
  136. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  137. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +89 -10
  138. package/plugins/lisa-cursor/hooks/discharge-work-item-gates.sh +7 -1
  139. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +44 -0
  140. package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +12 -0
  141. package/plugins/lisa-cursor/rules/00-rule-index.mdc +84 -0
  142. package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +97 -0
  143. package/plugins/lisa-cursor/rules/base-rules.mdc +10 -0
  144. package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +56 -0
  145. package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +58 -0
  146. package/plugins/lisa-cursor/rules/claim-archaeology-reference.mdc +42 -0
  147. package/plugins/lisa-cursor/rules/claim-evidence-mapping-reference.mdc +83 -0
  148. package/plugins/lisa-cursor/rules/claim-time-guards-reference.mdc +37 -0
  149. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +97 -0
  150. package/plugins/lisa-cursor/rules/config-resolution.mdc +1 -88
  151. package/plugins/lisa-cursor/rules/control-reachability-reference.mdc +44 -0
  152. package/plugins/lisa-cursor/rules/convergent-review-reference.mdc +31 -0
  153. package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +57 -0
  154. package/plugins/lisa-cursor/rules/dependency-decision-records-reference.mdc +37 -0
  155. package/plugins/lisa-cursor/rules/dependency-internalization-kit-reference.mdc +47 -0
  156. package/plugins/lisa-cursor/rules/dependency-trust-classes-reference.mdc +48 -0
  157. package/plugins/lisa-cursor/rules/deployed-state-readback-reference.mdc +52 -0
  158. package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +52 -0
  159. package/plugins/lisa-cursor/rules/design-source-of-truth-reference.mdc +43 -0
  160. package/plugins/lisa-cursor/rules/design-value-binding-reference.mdc +103 -0
  161. package/plugins/lisa-cursor/rules/documentation-source-paths-reference.mdc +18 -0
  162. package/plugins/lisa-cursor/rules/leaf-only-lifecycle-reference.mdc +75 -0
  163. package/plugins/lisa-cursor/rules/observability-audit-reference.mdc +20 -0
  164. package/plugins/lisa-cursor/rules/prd-definition-of-ready-reference.mdc +37 -0
  165. package/plugins/lisa-cursor/rules/prd-lifecycle-rollup-reference.mdc +36 -0
  166. package/plugins/lisa-cursor/rules/pre-flight-autofill-reference.mdc +88 -0
  167. package/plugins/lisa-cursor/rules/promotion-contract-reference.mdc +21 -0
  168. package/plugins/lisa-cursor/rules/readiness-rubric-reference.mdc +63 -0
  169. package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +34 -0
  170. package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +69 -0
  171. package/plugins/lisa-cursor/rules/repo-scope-split-reference.mdc +48 -0
  172. package/plugins/lisa-cursor/rules/reset-seed-coverage-reference.mdc +55 -0
  173. package/plugins/lisa-cursor/rules/usage-accounting-reference.mdc +38 -0
  174. package/plugins/lisa-cursor/rules/work-item-definition-of-ready-reference.mdc +32 -0
  175. package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +158 -1
  176. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  177. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +4 -0
  178. package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +4 -0
  179. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +4 -0
  180. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +4 -1
  181. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  182. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  183. package/plugins/lisa-expo-agy/plugin.json +1 -1
  184. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  185. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  186. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  187. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  188. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  189. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  190. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  191. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  192. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  193. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  194. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  195. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  196. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  197. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  198. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  199. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  200. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  201. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  202. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  203. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  204. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  205. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  206. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  207. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  208. package/plugins/lisa-rails-agy/plugin.json +1 -1
  209. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  210. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  211. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  212. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  213. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  214. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  215. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  216. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  217. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  218. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  219. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  220. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  221. package/plugins/src/base/hooks/block-direct-issue-create.sh +89 -10
  222. package/plugins/src/base/hooks/discharge-work-item-gates.sh +7 -1
  223. package/plugins/src/base/hooks/parity-safety-net.sh +44 -0
  224. package/plugins/src/base/hooks/threshold-ratchet-families.mjs +12 -0
  225. package/plugins/src/base/rules/eager/00-rule-index.md +79 -0
  226. package/plugins/src/base/rules/eager/base-rules.md +10 -0
  227. package/plugins/src/base/rules/eager/config-resolution.md +1 -88
  228. package/plugins/src/base/rules/reference/automation-runbook-contract.md +97 -0
  229. package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +56 -0
  230. package/plugins/src/base/rules/reference/blocker-containment.md +58 -0
  231. package/plugins/src/base/rules/reference/claim-archaeology.md +42 -0
  232. package/plugins/src/base/rules/reference/claim-evidence-mapping.md +83 -0
  233. package/plugins/src/base/rules/reference/claim-time-guards.md +37 -0
  234. package/plugins/src/base/rules/reference/config-resolution.md +97 -0
  235. package/plugins/src/base/rules/reference/control-reachability.md +44 -0
  236. package/plugins/src/base/rules/reference/convergent-review.md +31 -0
  237. package/plugins/src/base/rules/reference/credential-substrate-precedence.md +57 -0
  238. package/plugins/src/base/rules/reference/dependency-decision-records.md +37 -0
  239. package/plugins/src/base/rules/reference/dependency-internalization-kit.md +47 -0
  240. package/plugins/src/base/rules/reference/dependency-trust-classes.md +48 -0
  241. package/plugins/src/base/rules/reference/deployed-state-readback.md +52 -0
  242. package/plugins/src/base/rules/reference/derived-branch-plan.md +52 -0
  243. package/plugins/src/base/rules/reference/design-source-of-truth.md +43 -0
  244. package/plugins/src/base/rules/reference/design-value-binding.md +103 -0
  245. package/plugins/src/base/rules/reference/documentation-source-paths.md +18 -0
  246. package/plugins/src/base/rules/reference/leaf-only-lifecycle.md +75 -0
  247. package/plugins/src/base/rules/reference/observability-audit.md +20 -0
  248. package/plugins/src/base/rules/reference/prd-definition-of-ready.md +37 -0
  249. package/plugins/src/base/rules/reference/prd-lifecycle-rollup.md +36 -0
  250. package/plugins/src/base/rules/reference/pre-flight-autofill.md +88 -0
  251. package/plugins/src/base/rules/reference/promotion-contract.md +21 -0
  252. package/plugins/src/base/rules/reference/readiness-rubric.md +63 -0
  253. package/plugins/src/base/rules/reference/ready-role-filing.md +34 -0
  254. package/plugins/src/base/rules/reference/rejection-detection.md +69 -0
  255. package/plugins/src/base/rules/reference/repo-scope-split.md +48 -0
  256. package/plugins/src/base/rules/reference/reset-seed-coverage.md +55 -0
  257. package/plugins/src/base/rules/reference/usage-accounting.md +38 -0
  258. package/plugins/src/base/rules/reference/work-item-definition-of-ready.md +32 -0
  259. package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +158 -1
  260. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  261. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +4 -0
  262. package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +4 -0
  263. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +4 -0
  264. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +4 -1
  265. package/rails/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
  266. package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +12 -0
  267. package/scripts/check-eager-rule-budget.mjs +176 -0
  268. package/scripts/check-generated-artifact-merge-coverage.mjs +7 -0
  269. package/scripts/check-rules-pairing.sh +45 -14
  270. package/scripts/check-shipped-surface-removals.mjs +851 -0
  271. package/scripts/generate-export-surface.mjs +318 -0
  272. package/scripts/lib/shipped-surface.mjs +212 -0
  273. package/scripts/lisa-enforcement-fallback.sh +178 -1
  274. package/scripts/remedy-conformance.mjs +435 -0
  275. package/scripts/two-channel-couplings.json +2 -2
  276. package/typescript/copy-contents/.husky/pre-push +147 -2
  277. package/typescript/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
  278. package/typescript/copy-overwrite/audit.ignore.config.json +0 -10
  279. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +180 -29
  280. package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +12 -0
  281. package/typescript/package-lisa/package.lisa.json +72 -42
  282. package/plugins/lisa/rules/eager/automation-runbook-contract.md +0 -92
  283. package/plugins/lisa/rules/eager/bdd-e2e-coverage.md +0 -51
  284. package/plugins/lisa/rules/eager/blocker-containment.md +0 -53
  285. package/plugins/lisa/rules/eager/claim-archaeology.md +0 -37
  286. package/plugins/lisa/rules/eager/claim-evidence-mapping.md +0 -78
  287. package/plugins/lisa/rules/eager/claim-time-guards.md +0 -32
  288. package/plugins/lisa/rules/eager/control-reachability.md +0 -39
  289. package/plugins/lisa/rules/eager/convergent-review.md +0 -24
  290. package/plugins/lisa/rules/eager/credential-substrate-precedence.md +0 -54
  291. package/plugins/lisa/rules/eager/dependency-decision-records.md +0 -32
  292. package/plugins/lisa/rules/eager/dependency-internalization-kit.md +0 -42
  293. package/plugins/lisa/rules/eager/dependency-trust-classes.md +0 -43
  294. package/plugins/lisa/rules/eager/deployed-state-readback.md +0 -47
  295. package/plugins/lisa/rules/eager/derived-branch-plan.md +0 -47
  296. package/plugins/lisa/rules/eager/design-source-of-truth.md +0 -38
  297. package/plugins/lisa/rules/eager/design-value-binding.md +0 -98
  298. package/plugins/lisa/rules/eager/documentation-source-paths.md +0 -13
  299. package/plugins/lisa/rules/eager/factory-model.md +0 -41
  300. package/plugins/lisa/rules/eager/leaf-only-lifecycle.md +0 -70
  301. package/plugins/lisa/rules/eager/observability-audit.md +0 -15
  302. package/plugins/lisa/rules/eager/prd-definition-of-ready.md +0 -32
  303. package/plugins/lisa/rules/eager/prd-lifecycle-rollup.md +0 -31
  304. package/plugins/lisa/rules/eager/pre-flight-autofill.md +0 -84
  305. package/plugins/lisa/rules/eager/project-learnings.md +0 -22
  306. package/plugins/lisa/rules/eager/promotion-contract.md +0 -18
  307. package/plugins/lisa/rules/eager/readiness-rubric.md +0 -58
  308. package/plugins/lisa/rules/eager/ready-role-filing.md +0 -29
  309. package/plugins/lisa/rules/eager/rejection-detection.md +0 -64
  310. package/plugins/lisa/rules/eager/repo-scope-split.md +0 -43
  311. package/plugins/lisa/rules/eager/reset-seed-coverage.md +0 -50
  312. package/plugins/lisa/rules/eager/usage-accounting.md +0 -33
  313. package/plugins/lisa/rules/eager/work-item-definition-of-ready.md +0 -27
  314. package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +0 -92
  315. package/plugins/lisa-copilot/rules/eager/bdd-e2e-coverage.md +0 -51
  316. package/plugins/lisa-copilot/rules/eager/blocker-containment.md +0 -53
  317. package/plugins/lisa-copilot/rules/eager/claim-archaeology.md +0 -37
  318. package/plugins/lisa-copilot/rules/eager/claim-evidence-mapping.md +0 -78
  319. package/plugins/lisa-copilot/rules/eager/claim-time-guards.md +0 -32
  320. package/plugins/lisa-copilot/rules/eager/control-reachability.md +0 -39
  321. package/plugins/lisa-copilot/rules/eager/convergent-review.md +0 -24
  322. package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +0 -54
  323. package/plugins/lisa-copilot/rules/eager/dependency-decision-records.md +0 -32
  324. package/plugins/lisa-copilot/rules/eager/dependency-internalization-kit.md +0 -42
  325. package/plugins/lisa-copilot/rules/eager/dependency-trust-classes.md +0 -43
  326. package/plugins/lisa-copilot/rules/eager/deployed-state-readback.md +0 -47
  327. package/plugins/lisa-copilot/rules/eager/derived-branch-plan.md +0 -47
  328. package/plugins/lisa-copilot/rules/eager/design-source-of-truth.md +0 -38
  329. package/plugins/lisa-copilot/rules/eager/design-value-binding.md +0 -98
  330. package/plugins/lisa-copilot/rules/eager/documentation-source-paths.md +0 -13
  331. package/plugins/lisa-copilot/rules/eager/factory-model.md +0 -41
  332. package/plugins/lisa-copilot/rules/eager/leaf-only-lifecycle.md +0 -70
  333. package/plugins/lisa-copilot/rules/eager/observability-audit.md +0 -15
  334. package/plugins/lisa-copilot/rules/eager/prd-definition-of-ready.md +0 -32
  335. package/plugins/lisa-copilot/rules/eager/prd-lifecycle-rollup.md +0 -31
  336. package/plugins/lisa-copilot/rules/eager/pre-flight-autofill.md +0 -84
  337. package/plugins/lisa-copilot/rules/eager/project-learnings.md +0 -22
  338. package/plugins/lisa-copilot/rules/eager/promotion-contract.md +0 -18
  339. package/plugins/lisa-copilot/rules/eager/readiness-rubric.md +0 -58
  340. package/plugins/lisa-copilot/rules/eager/ready-role-filing.md +0 -29
  341. package/plugins/lisa-copilot/rules/eager/rejection-detection.md +0 -64
  342. package/plugins/lisa-copilot/rules/eager/repo-scope-split.md +0 -43
  343. package/plugins/lisa-copilot/rules/eager/reset-seed-coverage.md +0 -50
  344. package/plugins/lisa-copilot/rules/eager/usage-accounting.md +0 -33
  345. package/plugins/lisa-copilot/rules/eager/work-item-definition-of-ready.md +0 -27
  346. package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +0 -97
  347. package/plugins/lisa-cursor/rules/bdd-e2e-coverage.mdc +0 -56
  348. package/plugins/lisa-cursor/rules/blocker-containment.mdc +0 -58
  349. package/plugins/lisa-cursor/rules/claim-archaeology.mdc +0 -42
  350. package/plugins/lisa-cursor/rules/claim-evidence-mapping.mdc +0 -83
  351. package/plugins/lisa-cursor/rules/claim-time-guards.mdc +0 -37
  352. package/plugins/lisa-cursor/rules/control-reachability.mdc +0 -44
  353. package/plugins/lisa-cursor/rules/convergent-review.mdc +0 -29
  354. package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +0 -59
  355. package/plugins/lisa-cursor/rules/dependency-decision-records.mdc +0 -37
  356. package/plugins/lisa-cursor/rules/dependency-internalization-kit.mdc +0 -47
  357. package/plugins/lisa-cursor/rules/dependency-trust-classes.mdc +0 -48
  358. package/plugins/lisa-cursor/rules/deployed-state-readback.mdc +0 -52
  359. package/plugins/lisa-cursor/rules/derived-branch-plan.mdc +0 -52
  360. package/plugins/lisa-cursor/rules/design-source-of-truth.mdc +0 -43
  361. package/plugins/lisa-cursor/rules/design-value-binding.mdc +0 -103
  362. package/plugins/lisa-cursor/rules/documentation-source-paths.mdc +0 -18
  363. package/plugins/lisa-cursor/rules/factory-model.mdc +0 -46
  364. package/plugins/lisa-cursor/rules/leaf-only-lifecycle.mdc +0 -75
  365. package/plugins/lisa-cursor/rules/observability-audit.mdc +0 -20
  366. package/plugins/lisa-cursor/rules/prd-definition-of-ready.mdc +0 -37
  367. package/plugins/lisa-cursor/rules/prd-lifecycle-rollup.mdc +0 -36
  368. package/plugins/lisa-cursor/rules/pre-flight-autofill.mdc +0 -89
  369. package/plugins/lisa-cursor/rules/project-learnings.mdc +0 -27
  370. package/plugins/lisa-cursor/rules/promotion-contract.mdc +0 -23
  371. package/plugins/lisa-cursor/rules/readiness-rubric.mdc +0 -63
  372. package/plugins/lisa-cursor/rules/ready-role-filing.mdc +0 -34
  373. package/plugins/lisa-cursor/rules/rejection-detection.mdc +0 -69
  374. package/plugins/lisa-cursor/rules/repo-scope-split.mdc +0 -48
  375. package/plugins/lisa-cursor/rules/reset-seed-coverage.mdc +0 -55
  376. package/plugins/lisa-cursor/rules/usage-accounting.mdc +0 -38
  377. package/plugins/lisa-cursor/rules/work-item-definition-of-ready.mdc +0 -32
  378. package/plugins/src/base/rules/eager/automation-runbook-contract.md +0 -92
  379. package/plugins/src/base/rules/eager/bdd-e2e-coverage.md +0 -51
  380. package/plugins/src/base/rules/eager/blocker-containment.md +0 -53
  381. package/plugins/src/base/rules/eager/claim-archaeology.md +0 -37
  382. package/plugins/src/base/rules/eager/claim-evidence-mapping.md +0 -78
  383. package/plugins/src/base/rules/eager/claim-time-guards.md +0 -32
  384. package/plugins/src/base/rules/eager/control-reachability.md +0 -39
  385. package/plugins/src/base/rules/eager/convergent-review.md +0 -24
  386. package/plugins/src/base/rules/eager/credential-substrate-precedence.md +0 -54
  387. package/plugins/src/base/rules/eager/dependency-decision-records.md +0 -32
  388. package/plugins/src/base/rules/eager/dependency-internalization-kit.md +0 -42
  389. package/plugins/src/base/rules/eager/dependency-trust-classes.md +0 -43
  390. package/plugins/src/base/rules/eager/deployed-state-readback.md +0 -47
  391. package/plugins/src/base/rules/eager/derived-branch-plan.md +0 -47
  392. package/plugins/src/base/rules/eager/design-source-of-truth.md +0 -38
  393. package/plugins/src/base/rules/eager/design-value-binding.md +0 -98
  394. package/plugins/src/base/rules/eager/documentation-source-paths.md +0 -13
  395. package/plugins/src/base/rules/eager/factory-model.md +0 -41
  396. package/plugins/src/base/rules/eager/leaf-only-lifecycle.md +0 -70
  397. package/plugins/src/base/rules/eager/observability-audit.md +0 -15
  398. package/plugins/src/base/rules/eager/prd-definition-of-ready.md +0 -32
  399. package/plugins/src/base/rules/eager/prd-lifecycle-rollup.md +0 -31
  400. package/plugins/src/base/rules/eager/pre-flight-autofill.md +0 -84
  401. package/plugins/src/base/rules/eager/project-learnings.md +0 -22
  402. package/plugins/src/base/rules/eager/promotion-contract.md +0 -18
  403. package/plugins/src/base/rules/eager/readiness-rubric.md +0 -58
  404. package/plugins/src/base/rules/eager/ready-role-filing.md +0 -29
  405. package/plugins/src/base/rules/eager/rejection-detection.md +0 -64
  406. package/plugins/src/base/rules/eager/repo-scope-split.md +0 -43
  407. package/plugins/src/base/rules/eager/reset-seed-coverage.md +0 -50
  408. package/plugins/src/base/rules/eager/usage-accounting.md +0 -33
  409. package/plugins/src/base/rules/eager/work-item-definition-of-ready.md +0 -27
@@ -273,6 +273,178 @@ resolve_vintages() {
273
273
  plugin_tree_version="$json_version"
274
274
  fi
275
275
  note_version "$plugin_tree_version" "$plugin_tree"
276
+
277
+ # The channel this dispatcher races. Deliberately NOT folded into
278
+ # `note_version`: the newest-on-disk maximum answers "is this copy behind
279
+ # something local", and the plugin channel's age is a separate question
280
+ # about a copy that runs in parallel rather than instead.
281
+ resolve_plugin_channel || true
282
+ }
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # The other channel
286
+ #
287
+ # YOU CANNOT RETIRE A REFUSAL BY SHIPPING A FIX.
288
+ #
289
+ # These guards reach an agent by two independent channels: this dispatcher,
290
+ # registered in `.claude/settings.json`, and the plugin manifest, which
291
+ # registers the same guards individually. An agent sees the UNION of the two
292
+ # verdicts, so a TIGHTENING on either channel takes effect at once while a
293
+ # RELAXATION is inert until the slower channel catches up — the stale copy goes
294
+ # on refusing, and its refusal wins. What the operator sees is a guard blocking
295
+ # something `main` already permits, which reads as the guard being WRONG rather
296
+ # than OLD, and the move that reading suggests is to route around it.
297
+ #
298
+ # The two channels are two different files refreshed by two different
299
+ # mechanisms: `scripts/lisa-hooks/` when `lisa apply` runs, the plugin's own
300
+ # `hooks/` when the plugin updates. They drift. Measured on one machine on one
301
+ # day, two checkouts of the SAME repository resolved plugin 4.32.2 and 4.47.0.
302
+ #
303
+ # `plugin_tree` above cannot see any of that: it is repo-relative, so it exists
304
+ # only inside the Lisa monorepo, and in a host project the plugin actually in
305
+ # force is somewhere else entirely. The vintage machinery written for #3205
306
+ # was therefore blind to the channel it races. This resolves it properly, from
307
+ # the record the runtime itself keeps.
308
+ #
309
+ # WHAT THIS RECORD CAN AND CANNOT ANSWER. The header above explains why a
310
+ # previous stand-down keyed on `installed_plugins.json` was removed: the record
311
+ # is project-blind as it was read then, enablement-blind (`enabledPlugins` can
312
+ # switch a plugin off without removing its entry), and session-blind (hooks load
313
+ # at session start; the record is rewritten on any install). Those defeat a
314
+ # LIVENESS question — "are the plugin's guards running in this session?" — and
315
+ # nothing on disk answers that one.
316
+ #
317
+ # This asks a different question: "what VINTAGE is the plugin installed for this
318
+ # project?" The record answers that directly, and the project-blindness is
319
+ # resolved by keying the lookup on this repo root rather than on the plugin name.
320
+ # The remaining two blindnesses bound the claim rather than break it: a skew
321
+ # reported here is a real difference between two copies on disk, but a disabled
322
+ # or not-yet-loaded plugin may mean those copies are not both firing right now.
323
+ # So the wording below reports what is INSTALLED and what the union WOULD mean,
324
+ # and never asserts that both channels are live. Reporting is also why this may
325
+ # use the record at all where a stand-down may not: an over-reported skew costs
326
+ # a line of notice, while an over-confident stand-down costs enforcement.
327
+ plugin_channel_version=""
328
+ plugin_channel_path=""
329
+
330
+ # Resolve the vintage of the guard channel the PLUGIN MANIFEST runs.
331
+ #
332
+ # `plugins/installed_plugins.json` records installs PER PROJECT DIRECTORY —
333
+ # which is exactly why one checkout sits versions behind another on the same
334
+ # disk — so the lookup is keyed on this repo root, not on the machine.
335
+ #
336
+ # A record is claimed only when its `installPath` sits under a `lisa/lisa/`
337
+ # marketplace cache. A locally-installed plugin does not match and is left
338
+ # UNRESOLVED on purpose: reporting "agree" about a copy that was never read is
339
+ # the failure this whole section exists to end, and an honest "I could not
340
+ # tell" is the safe direction.
341
+ #
342
+ # `grep` does the scanning because the record runs to tens of thousands of
343
+ # lines; the bash loop then sees a handful. Called only from resolve_vintages,
344
+ # so it inherits that latch and never runs on the silent allow path.
345
+ resolve_plugin_channel() {
346
+ local config_dir="${CLAUDE_CONFIG_DIR-}"
347
+ [ -n "$config_dir" ] || config_dir="${HOME-}/.claude"
348
+ local record="$config_dir/plugins/installed_plugins.json"
349
+ [ -f "$record" ] || return 1
350
+
351
+ local install_pattern="^[[:space:]]*\"installPath\"[[:space:]]*:[[:space:]]*\"(.*/lisa/lisa/[^\"]+)\""
352
+ local version_pattern="^[[:space:]]*\"version\"[[:space:]]*:[[:space:]]*\"([^\"]+)\""
353
+ local line
354
+ local candidate=""
355
+ while IFS= read -r line || [ -n "$line" ]; do
356
+ # `grep -A` separates non-adjacent groups with `--`. A candidate must not
357
+ # survive across that boundary or one plugin's installPath answers for
358
+ # another plugin's version.
359
+ if [ "$line" = "--" ]; then
360
+ candidate=""
361
+ continue
362
+ fi
363
+ if [[ "$line" =~ $install_pattern ]]; then
364
+ candidate="${BASH_REMATCH[1]}"
365
+ continue
366
+ fi
367
+ if [ -n "$candidate" ] && [[ "$line" =~ $version_pattern ]]; then
368
+ plugin_channel_path="$candidate"
369
+ plugin_channel_version="${BASH_REMATCH[1]}"
370
+ return 0
371
+ fi
372
+ done < <(grep -F -A4 "\"projectPath\": \"$repo_root\"," "$record" 2>/dev/null)
373
+ return 1
374
+ }
375
+
376
+ # Verdict of the last classify_channel_skew call: agree | skew | undetermined.
377
+ #
378
+ # Three answers, never two. "The channels agree" and "I could not read one of
379
+ # them" are DIFFERENT facts, and collapsing them is how a probe reports success
380
+ # while measuring nothing.
381
+ #
382
+ # THIS PROBE REPORTS. IT NEVER BLOCKS. A permitted command stays permitted
383
+ # whatever the verdict, and the finding goes into the once-per-session notice.
384
+ # That is a decision, not an oversight, so here is the reasoning for whoever
385
+ # comes to change it:
386
+ #
387
+ # - The two error directions have asymmetric costs. An over-reported skew
388
+ # costs a line of notice; an over-confident stand-down costs enforcement.
389
+ # When the costs are that lopsided the direction is settled without needing
390
+ # to argue about likelihood.
391
+ # - Failing closed on `undetermined` would refuse work on every host with no
392
+ # `installed_plugins.json` — containers, fresh clones, non-Claude runtimes.
393
+ # That is a large population with nothing wrong with it, and refusing them
394
+ # is a bigger fault than the one being detected.
395
+ # - What is detected is two copies at different AGES, not a compromised
396
+ # guard. Blocking on it turns a diagnostic into an outage.
397
+ # - And the practical argument, which beats the principled one: skew is the
398
+ # common case on a developer machine right now, so as a blocker this would
399
+ # be a permanent stop-work and the first response would be to switch it
400
+ # off. A blocker everyone turns off protects nothing.
401
+ #
402
+ # Pinned by the test named "lets a permitted command through whatever the
403
+ # verdict is", which asserts exit 0 on both the skew and undetermined arms.
404
+ channel_skew_verdict="undetermined"
405
+ channel_skew_line=""
406
+
407
+ # Compare the vintage of the channel this dispatcher runs against the vintage
408
+ # of the channel the plugin manifest runs.
409
+ classify_channel_skew() {
410
+ local mine=""
411
+ if [ "$host_tree_used" -eq 1 ]; then
412
+ mine="$host_tree_version"
413
+ elif [ "$plugin_tree_used" -eq 1 ]; then
414
+ mine="$plugin_tree_version"
415
+ fi
416
+
417
+ if [ -z "$mine" ] || [ -z "$plugin_channel_version" ]; then
418
+ channel_skew_verdict="undetermined"
419
+ channel_skew_line=" cross-channel vintage UNDETERMINED — this dispatcher's guards could not be
420
+ compared with the plugin manifest's copies. Not agreement: an unread copy
421
+ can be any age, and a relaxation shipped to one channel stays inert until
422
+ the other catches up.
423
+ "
424
+ return 0
425
+ fi
426
+
427
+ if [ "$mine" = "$plugin_channel_version" ]; then
428
+ channel_skew_verdict="agree"
429
+ channel_skew_line=""
430
+ return 0
431
+ fi
432
+
433
+ channel_skew_verdict="skew"
434
+ # The sentence is kept whole on one line on purpose: it is the finding, and
435
+ # an operator greps for it.
436
+ channel_skew_line=" cross-channel vintage SKEW — this dispatcher runs lisa $mine; the plugin
437
+ installed for this project is lisa $plugin_channel_version at $plugin_channel_path.
438
+ When both fire on one tool call the agent sees the UNION of their verdicts,
439
+ which means:
440
+ YOU CANNOT RETIRE A REFUSAL BY SHIPPING A FIX.
441
+ A relaxation on the newer channel does nothing until the older one is
442
+ refreshed, so a block you cannot explain from \`main\` is the older copy
443
+ still enforcing.
444
+ repair: refresh both — \`npx @codyswann/lisa apply\` for this checkout's
445
+ guards, and update the installed plugin for the manifest's copies.
446
+ "
447
+ return 0
276
448
  }
277
449
 
278
450
  # Description of the last describe_vintage call.
@@ -610,8 +782,10 @@ if [ "$notice_due" -eq 1 ]; then
610
782
  if [ "$plugin_tree_used" -eq 1 ]; then
611
783
  note_tree_staleness "$plugin_tree" "$plugin_tree_version" "$PLUGIN_REPAIR"
612
784
  fi
785
+ classify_channel_skew
613
786
 
614
- if [ -n "$stale_notice" ] || [ -n "$shadowed" ] || [ -n "$missing" ]; then
787
+ if [ -n "$stale_notice" ] || [ -n "$shadowed" ] || [ -n "$missing" ] || \
788
+ [ -n "$channel_skew_line" ]; then
615
789
  # Claim the session BEFORE printing. A failed `mkdir` suppresses this copy
616
790
  # only when another process left the expected real directory behind. Every
617
791
  # other failure leaves the claim unproven and prints, so an unwritable state
@@ -647,6 +821,9 @@ if [ "$notice_due" -eq 1 ]; then
647
821
  if [ -n "$missing" ]; then
648
822
  printf ' unresolved guards: %s (no copy was dispatched)\n' "$missing"
649
823
  fi
824
+ if [ -n "$channel_skew_line" ]; then
825
+ printf '%s' "$channel_skew_line"
826
+ fi
650
827
  } >&2
651
828
  fi
652
829
  fi
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Every remedy a guard prints must be one this environment permits (#3825).
4
+ *
5
+ * ## The defect
6
+ *
7
+ * A guard refuses correctly and then hands the operator an instruction that
8
+ * cannot work. The guard is fine; its remedy is the defect — and the failure is
9
+ * misattributed by construction, because when an agent follows the printed
10
+ * advice and is refused, the refusal names **the guard it obeyed**, not the
11
+ * advice that sent it there. So the evidence points away from the actual bug.
12
+ *
13
+ * Nothing executed a remedy string, so a remedy rotted exactly like a comment,
14
+ * except an agent will *act* on it. Three guards, three unrelated authors,
15
+ * accumulated broken remedies before anyone named the class.
16
+ *
17
+ * ## Why permission alone is not the check — measured, not assumed
18
+ *
19
+ * The obvious mechanism is "run the remedy through the guard chain, assert
20
+ * permitted". **That mechanism would have passed the instance that motivated
21
+ * the ticket.** Measured on this repository: `git stash push` is PERMITTED by
22
+ * `parity-safety-net.sh` — guard 7 refuses only `stash drop`/`clear`. So when a
23
+ * guard advised "use git stash to preserve work first", a permission check
24
+ * would have gone green while the advice pointed every reader at one stash
25
+ * stack shared by every worktree of the clone, racing concurrent agents.
26
+ *
27
+ * A remedy is therefore conformant only when BOTH hold:
28
+ *
29
+ * 1. the guard chain PERMITS it — nothing refuses the operator who obeys; and
30
+ * 2. it names no FORBIDDEN operation — an operation this repository has ruled
31
+ * unsafe for reasons no guard enforces at the command level.
32
+ *
33
+ * Check 2 exists precisely because check 1 cannot see hazards that are policy
34
+ * rather than pattern.
35
+ *
36
+ * ## Fail direction
37
+ *
38
+ * **Report-only on its findings, fail-closed on its own blindness.** It gates
39
+ * no commit and no push; a broken remedy is a documentation defect, and a guard
40
+ * that refused a contributor's push because some message was worded badly would
41
+ * be worse than the defect. But a probe whose guard did not refuse produced NO
42
+ * remedy text, and counting that as "no unfollowable remedies" is the reassuring
43
+ * answer for a measurement that never happened. Those become `NOT_EXAMINED`,
44
+ * and any of them makes the sweep `NOT_MEASURED` rather than `CONFORMING`.
45
+ * Exit codes carry the same split: 0 conforming, 1 findings, 2 not measured.
46
+ */
47
+
48
+ import { spawnSync } from "node:child_process";
49
+ import { realpathSync } from "node:fs";
50
+ import { fileURLToPath } from "node:url";
51
+
52
+ /** Per-remedy verdicts. */
53
+ export const REMEDY_VERDICTS = Object.freeze([
54
+ "PERMITTED",
55
+ "REFUSED",
56
+ "FORBIDDEN",
57
+ ]);
58
+
59
+ /** Whole-sweep verdicts. */
60
+ export const REMEDY_SWEEP_VERDICTS = Object.freeze([
61
+ "CONFORMING",
62
+ "UNFOLLOWABLE_REMEDIES",
63
+ "NOT_MEASURED",
64
+ ]);
65
+
66
+ /**
67
+ * Operations no remedy may recommend, whatever the guard chain permits.
68
+ *
69
+ * Each entry is a ruling this repository has already made and written down
70
+ * somewhere other than a command pattern. `git stash` is the founding member:
71
+ * `PRESERVE_GUIDANCE` in `parity-safety-net.sh` says "Do NOT reach for `git
72
+ * stash`. One stash stack is shared by every worktree of a clone, so a
73
+ * concurrently running agent can consume the entry you just pushed" — and yet
74
+ * the guard permits the command, because refusing it outright would break the
75
+ * legitimate single-worktree uses.
76
+ *
77
+ * `advisory` is what separates a recommendation from a prohibition. Guard 7's
78
+ * own message must keep containing the word "stash" while REFUSING it; only
79
+ * text that offers the operation as the way forward is a finding. Keying on the
80
+ * bare token would fail the one message doing the right thing — the mistake
81
+ * `parity-safety-net-no-stash-advice.test.ts` records making.
82
+ */
83
+ export const FORBIDDEN_REMEDY_OPERATIONS = Object.freeze([
84
+ Object.freeze({
85
+ id: "shared-stash",
86
+ advisory: /\bgit\s+stash\s+(push|pop|save|apply)\b/i,
87
+ why: "one stash stack is shared by every worktree of a clone, so a concurrent agent can consume the entry",
88
+ }),
89
+ ]);
90
+
91
+ /** Command verbs a remedy line may begin with to count as a command. */
92
+ const REMEDY_VERBS = /^(git|gh|node|bun|npm|npx|bash|sh|mktemp|jq)\b/;
93
+
94
+ /**
95
+ * Markers that make a line talk ABOUT a command rather than instruct it.
96
+ *
97
+ * **Measured, not anticipated.** The first live run of this sweep reported six
98
+ * findings, every one of them the same string pulled out of
99
+ * `PRESERVE_GUIDANCE`'s sentence "Untracked files survive, exactly as they
100
+ * survive ...". That is a comparison. The guard is not telling anyone to run
101
+ * that command — it refuses it two guards further up the same file.
102
+ *
103
+ * So the extractor was making exactly the mistake #3815 and this ticket's own
104
+ * second comment describe: treating text ABOUT a thing as the thing. A
105
+ * conformance control that makes it reports its loudest findings against
106
+ * correct prose, and a control whose findings are wrong on day one gets
107
+ * switched off. That outcome matters more here than a missed remedy, because
108
+ * the entire value of this control is that somebody reads its output.
109
+ *
110
+ * The marker is looked for in a WINDOW of text immediately preceding the
111
+ * command, not on its line and not in its paragraph. Both of those were tried
112
+ * and both were measured wrong on this repository's real guard text:
113
+ *
114
+ * - LINE scope missed the founding case outright. The heredoc wraps, so
115
+ * "exactly as they" ends one line and "survive `git reset --hard`." begins
116
+ * the next; the marker and the command it qualifies are never on one line.
117
+ * - PARAGRAPH scope demotes correct remedies wholesale. `DESTRUCTIVE_GUIDANCE`
118
+ * opens a bullet with "Do not try to reword it" and then names three real
119
+ * remedies in the same bullet, all of which would vanish from the sweep.
120
+ *
121
+ * The window approximates the clause the command sits in, which is the scope
122
+ * the qualifier actually applies to. Mentions are not classified, and they are
123
+ * COUNTED AND REPORTED rather than dropped, so the coverage this rule costs
124
+ * stays visible instead of being silently absorbed.
125
+ */
126
+ const MENTION_CONTEXT =
127
+ /\b(do not|don't|never|no longer|used to|as they survive|instead of|rather than|cannot|would|refuses?|blocks?|prohibit)/i;
128
+
129
+ /** How much text before a command is read for a mention marker. */
130
+ const MENTION_WINDOW = 64;
131
+
132
+ /**
133
+ * Pull the runnable commands out of a guard's refusal text.
134
+ *
135
+ * Two shapes, because guards use both: a backticked span, and an indented line
136
+ * inside a heredoc. Prose is skipped by requiring a known verb at the start —
137
+ * a remedy sentence is advice, and only its commands can be classified.
138
+ *
139
+ * A placeholder like `<path>` is substituted with a plausible path so the
140
+ * command reaches the guard in the shape an operator would actually type;
141
+ * leaving the angle brackets in would feed the guard a redirection.
142
+ *
143
+ * @param {string} text - A guard's refusal output.
144
+ * @returns {{ commands: readonly string[], mentions: readonly string[] }}
145
+ * Instructions to classify, and commands the text only talks about.
146
+ */
147
+ export function extractRemedyCommands(text) {
148
+ const source = typeof text === "string" ? text : "";
149
+ const commands = [];
150
+ const mentions = [];
151
+
152
+ // Backticked spans, judged against the clause that precedes each one. The
153
+ // source is flattened so a wrapped qualifier still sits inside the window.
154
+ const flat = source.replace(/\s+/g, " ");
155
+ for (const match of flat.matchAll(/`([^`]+)`/g)) {
156
+ const candidate = match[1].trim();
157
+ if (!REMEDY_VERBS.test(candidate)) continue;
158
+ const before = flat.slice(
159
+ Math.max(0, match.index - MENTION_WINDOW),
160
+ match.index
161
+ );
162
+ (MENTION_CONTEXT.test(before) ? mentions : commands).push(candidate);
163
+ }
164
+
165
+ // Indented lines are code blocks, which are instructions by convention even
166
+ // where the surrounding prose is comparative.
167
+ for (const line of source.split("\n")) {
168
+ if (!/^\s{2,}\S/.test(line)) continue;
169
+ const trimmed = line.trim();
170
+ if (REMEDY_VERBS.test(trimmed) || /^[a-z_]+="\$\(/.test(trimmed)) {
171
+ commands.push(trimmed);
172
+ }
173
+ }
174
+
175
+ const instructions = new Set(commands.map(substitutePlaceholders));
176
+ return {
177
+ commands: [...instructions],
178
+ // A command that appears BOTH as an instruction and as a mention is an
179
+ // instruction: the code block is the operative text.
180
+ mentions: [...new Set(mentions)].filter(
181
+ mention => !instructions.has(substitutePlaceholders(mention))
182
+ ),
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Replace `<placeholder>` tokens with a plausible concrete path.
188
+ *
189
+ * @param {string} command - A command that may carry placeholders.
190
+ * @returns {string} The command with placeholders made concrete.
191
+ */
192
+ function substitutePlaceholders(command) {
193
+ return command.replace(/<[a-z][a-z -]*>/gi, "tmp/remedy-conformance-probe");
194
+ }
195
+
196
+ /**
197
+ * Classify one remedy command.
198
+ *
199
+ * Forbidden beats refused: a remedy naming the shared stash is a finding even
200
+ * where the guard chain waves it through, which is the whole reason check 2
201
+ * exists.
202
+ *
203
+ * @param {string} command - The remedy command as printed.
204
+ * @param {(command: string) => number | null} permits - Guard-chain classifier,
205
+ * returning the guard's exit status for the command.
206
+ * @returns {{ command: string, verdict: string, reason: string }} The verdict.
207
+ */
208
+ export function classifyRemedy(command, permits) {
209
+ const forbidden = FORBIDDEN_REMEDY_OPERATIONS.find(operation =>
210
+ operation.advisory.test(command)
211
+ );
212
+ if (forbidden) {
213
+ return {
214
+ command,
215
+ verdict: "FORBIDDEN",
216
+ reason: `${forbidden.id}: ${forbidden.why}`,
217
+ };
218
+ }
219
+
220
+ const status = permits(command);
221
+ return status === 0
222
+ ? { command, verdict: "PERMITTED", reason: "guard-chain-permits" }
223
+ : {
224
+ command,
225
+ verdict: "REFUSED",
226
+ reason: `guard-chain-refuses (exit ${status})`,
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Sweep a set of probes and report every unfollowable remedy.
232
+ *
233
+ * @param {{
234
+ * readonly probes?: readonly { label: string, refusal: string | null }[]
235
+ * readonly permits?: (command: string) => number | null
236
+ * }} input - `refusal` is the guard's output, or `null` when the guard did not
237
+ * refuse and therefore produced no remedy to examine.
238
+ * @returns {{
239
+ * readonly verdict: string
240
+ * readonly reasons: readonly string[]
241
+ * readonly examinedCount: number
242
+ * readonly commandCount: number
243
+ * readonly mentionCount: number
244
+ * readonly findings: readonly {
245
+ * readonly command: string
246
+ * readonly verdict: string
247
+ * readonly reason: string
248
+ * readonly probe: string
249
+ * }[]
250
+ * readonly notExamined: readonly string[]
251
+ * readonly reportOnly: true
252
+ * }} The sweep result.
253
+ */
254
+ export function sweepRemedyConformance(input = {}) {
255
+ const probes = Array.isArray(input.probes) ? input.probes : null;
256
+ const permits = input.permits ?? (() => 0);
257
+
258
+ if (probes === null) {
259
+ return emptySweep("NOT_MEASURED", ["probe-list-unavailable"]);
260
+ }
261
+
262
+ const notExamined = probes
263
+ .filter(probe => typeof probe?.refusal !== "string")
264
+ .map(probe => String(probe?.label ?? "<unlabelled>"));
265
+
266
+ const findings = [];
267
+ let commandCount = 0;
268
+ let mentionCount = 0;
269
+ for (const probe of probes) {
270
+ if (typeof probe?.refusal !== "string") continue;
271
+ const extracted = extractRemedyCommands(probe.refusal);
272
+ mentionCount += extracted.mentions.length;
273
+ for (const command of extracted.commands) {
274
+ commandCount += 1;
275
+ const verdict = classifyRemedy(command, permits);
276
+ if (verdict.verdict !== "PERMITTED") {
277
+ findings.push({ ...verdict, probe: probe.label });
278
+ }
279
+ }
280
+ }
281
+
282
+ const examinedCount = probes.length - notExamined.length;
283
+
284
+ // Blindness outranks findings: a sweep that read no remedy text from part of
285
+ // the guard set cannot call the rest of it conformant.
286
+ if (notExamined.length > 0) {
287
+ return {
288
+ verdict: "NOT_MEASURED",
289
+ reasons: ["guard-did-not-refuse"],
290
+ examinedCount,
291
+ commandCount,
292
+ mentionCount,
293
+ findings,
294
+ notExamined,
295
+ reportOnly: true,
296
+ };
297
+ }
298
+
299
+ return {
300
+ verdict: findings.length > 0 ? "UNFOLLOWABLE_REMEDIES" : "CONFORMING",
301
+ reasons:
302
+ findings.length > 0
303
+ ? ["remedies-the-environment-refuses-or-forbids"]
304
+ : ["every-printed-remedy-permitted-and-allowed"],
305
+ examinedCount,
306
+ commandCount,
307
+ mentionCount,
308
+ findings,
309
+ notExamined,
310
+ reportOnly: true,
311
+ };
312
+ }
313
+
314
+ /**
315
+ * A sweep result carrying nothing, for the paths that measured nothing.
316
+ *
317
+ * @param {string} verdict - The sweep verdict.
318
+ * @param {readonly string[]} reasons - Why.
319
+ * @returns {ReturnType<typeof sweepRemedyConformance>} The empty sweep.
320
+ */
321
+ function emptySweep(verdict, reasons) {
322
+ return {
323
+ verdict,
324
+ reasons,
325
+ examinedCount: 0,
326
+ commandCount: 0,
327
+ mentionCount: 0,
328
+ findings: [],
329
+ notExamined: [],
330
+ reportOnly: true,
331
+ };
332
+ }
333
+
334
+ /**
335
+ * The CLI exit status for a sweep.
336
+ *
337
+ * `2` is load-bearing: a sweep that examined nothing must not exit 0 beside an
338
+ * empty finding list, which is indistinguishable from a conformant guard set.
339
+ *
340
+ * @param {{ readonly verdict?: string }} sweep - A sweep result.
341
+ * @returns {0 | 1 | 2} The exit status.
342
+ */
343
+ export function remedyConformanceExitCode(sweep) {
344
+ if (sweep?.verdict === "CONFORMING") return 0;
345
+ if (sweep?.verdict === "UNFOLLOWABLE_REMEDIES") return 1;
346
+ return 2;
347
+ }
348
+
349
+ /**
350
+ * Render the sweep for an operator.
351
+ *
352
+ * @param {ReturnType<typeof sweepRemedyConformance>} sweep - A sweep result.
353
+ * @returns {string} A terminal-first report.
354
+ */
355
+ export function formatRemedyConformanceReport(sweep) {
356
+ const lines = [`Remedy conformance: ${sweep.verdict}`];
357
+
358
+ if (sweep.verdict === "NOT_MEASURED") {
359
+ lines.push(
360
+ `No remedy text was read for ${sweep.notExamined.length} probe(s) (${sweep.reasons.join(", ")}).`,
361
+ "This is not a conformant guard set — it is an unanswered question.",
362
+ ...sweep.notExamined.map(label => ` ? ${label}`)
363
+ );
364
+ return `${lines.join("\n")}\n`;
365
+ }
366
+
367
+ lines.push(
368
+ `Examined ${sweep.examinedCount} guard refusal(s) and classified ${sweep.commandCount} printed command(s).`,
369
+ `Not classified: ${sweep.mentionCount} command(s) the text only mentions (comparisons and prohibitions).`
370
+ );
371
+
372
+ if (sweep.verdict === "CONFORMING") {
373
+ lines.push(
374
+ "Every command any examined guard prints is permitted here and names no forbidden operation."
375
+ );
376
+ return `${lines.join("\n")}\n`;
377
+ }
378
+
379
+ lines.push("These printed remedies cannot be followed as written:");
380
+ for (const finding of sweep.findings) {
381
+ lines.push(` ! [${finding.probe}] ${finding.command}`);
382
+ lines.push(` ${finding.verdict}: ${finding.reason}`);
383
+ }
384
+ return `${lines.join("\n")}\n`;
385
+ }
386
+
387
+ /**
388
+ * Build a guard-chain classifier that runs the real shipped guard.
389
+ *
390
+ * @param {string} guardPath - Path to the shipped guard script.
391
+ * @param {string} cwd - Working directory reported to the guard.
392
+ * @returns {(command: string) => number | null} The classifier.
393
+ */
394
+ export function guardChainClassifier(guardPath, cwd) {
395
+ return command =>
396
+ spawnSync("/bin/bash", [guardPath], {
397
+ input: JSON.stringify({
398
+ tool_name: "Bash",
399
+ tool_input: { command },
400
+ cwd,
401
+ }),
402
+ encoding: "utf8",
403
+ }).status;
404
+ }
405
+
406
+ /**
407
+ * True when `moduleUrl` names the module node was asked to run.
408
+ *
409
+ * @param {string} moduleUrl - The caller's own `import.meta.url`.
410
+ * @param {string | undefined} [argv1] - Entry path.
411
+ * @returns {boolean} Whether to run the CLI body.
412
+ */
413
+ export function invokedAsScript(moduleUrl, argv1 = process.argv[1]) {
414
+ if (!argv1) return false;
415
+ try {
416
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
417
+ } catch {
418
+ return false;
419
+ }
420
+ }
421
+
422
+ if (invokedAsScript(import.meta.url)) {
423
+ const chunks = [];
424
+ for await (const chunk of process.stdin) chunks.push(chunk);
425
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
426
+ const sweep = sweepRemedyConformance({
427
+ probes: raw.length === 0 ? null : JSON.parse(raw).probes,
428
+ permits: guardChainClassifier(
429
+ process.argv[2] ?? "plugins/lisa/hooks/parity-safety-net.sh",
430
+ process.cwd()
431
+ ),
432
+ });
433
+ process.stdout.write(formatRemedyConformanceReport(sweep));
434
+ process.exitCode = remedyConformanceExitCode(sweep);
435
+ }
@@ -4,8 +4,8 @@
4
4
  "quality.yml::scripts/generate-nightly-e2e-guard-certificate.mjs": "Lisa-source-only branch. Both reusable jobs require `github.repository == 'CodySwannGT/lisa'` before the step can start, and the run body repeats that exact repository guard before checking this artifact path. Ordinary and private host callers therefore skip the step without reading or fetching from their own tree. The scoped branch fetches only the immutable refs needed to behavior-test Lisa's own retained certificate inputs. Ratified because the caller-tree read is unreachable outside this repository, not because an undelivered host dependency is tolerated."
5
5
  },
6
6
  "inspected": {
7
- "workflows": 22,
8
- "steps": 716,
7
+ "workflows": 23,
8
+ "steps": 719,
9
9
  "couplings": 34,
10
10
  "inventory": 77
11
11
  },