@codyswann/lisa 4.48.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 (426) 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/configs/vitest/base.d.ts +71 -1
  11. package/dist/configs/vitest/base.d.ts.map +1 -1
  12. package/dist/configs/vitest/base.js +141 -23
  13. package/dist/configs/vitest/base.js.map +1 -1
  14. package/dist/configs/vitest/fleet-admission-global-setup.d.ts +34 -0
  15. package/dist/configs/vitest/fleet-admission-global-setup.d.ts.map +1 -0
  16. package/dist/configs/vitest/fleet-admission-global-setup.js +123 -0
  17. package/dist/configs/vitest/fleet-admission-global-setup.js.map +1 -0
  18. package/dist/configs/vitest/fleet-admission.d.ts +129 -0
  19. package/dist/configs/vitest/fleet-admission.d.ts.map +1 -0
  20. package/dist/configs/vitest/fleet-admission.js +239 -0
  21. package/dist/configs/vitest/fleet-admission.js.map +1 -0
  22. package/dist/configs/vitest/synthesis-scratch-reclaim.d.ts +86 -0
  23. package/dist/configs/vitest/synthesis-scratch-reclaim.d.ts.map +1 -0
  24. package/dist/configs/vitest/synthesis-scratch-reclaim.js +226 -0
  25. package/dist/configs/vitest/synthesis-scratch-reclaim.js.map +1 -0
  26. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  27. package/dist/core/lisa-owned-hash-ledger.js +76 -0
  28. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  29. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  30. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  31. package/dist/core/upstream-evidence-manifest.js +129 -228
  32. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  33. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +45 -1
  34. package/dist/opencode/plugin-templates/parity-safety-net.sh +44 -0
  35. package/expo/package-lisa/package.lisa.json +83 -53
  36. package/harper-fabric/package-lisa/package.lisa.json +62 -32
  37. package/nestjs/package-lisa/package.lisa.json +57 -27
  38. package/npm-package/package-lisa/package.lisa.json +43 -7
  39. package/package.json +11 -7
  40. package/phaser/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
  41. package/phaser/package-lisa/package.lisa.json +62 -32
  42. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  45. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +4 -0
  46. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +4 -0
  47. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +4 -0
  48. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +4 -1
  49. package/plugins/lisa/hooks/block-direct-issue-create.sh +89 -10
  50. package/plugins/lisa/hooks/discharge-work-item-gates.sh +7 -1
  51. package/plugins/lisa/hooks/parity-safety-net.sh +44 -0
  52. package/plugins/lisa/hooks/threshold-ratchet-families.mjs +12 -0
  53. package/plugins/lisa/rules/eager/00-rule-index.md +79 -0
  54. package/plugins/lisa/rules/eager/base-rules.md +10 -0
  55. package/plugins/lisa/rules/eager/config-resolution.md +1 -88
  56. package/plugins/lisa/rules/reference/automation-runbook-contract.md +97 -0
  57. package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +56 -0
  58. package/plugins/lisa/rules/reference/blocker-containment.md +58 -0
  59. package/plugins/lisa/rules/reference/claim-archaeology.md +42 -0
  60. package/plugins/lisa/rules/reference/claim-evidence-mapping.md +83 -0
  61. package/plugins/lisa/rules/reference/claim-time-guards.md +37 -0
  62. package/plugins/lisa/rules/reference/config-resolution.md +97 -0
  63. package/plugins/lisa/rules/reference/control-reachability.md +44 -0
  64. package/plugins/lisa/rules/reference/convergent-review.md +31 -0
  65. package/plugins/lisa/rules/reference/credential-substrate-precedence.md +57 -0
  66. package/plugins/lisa/rules/reference/dependency-decision-records.md +37 -0
  67. package/plugins/lisa/rules/reference/dependency-internalization-kit.md +47 -0
  68. package/plugins/lisa/rules/reference/dependency-trust-classes.md +48 -0
  69. package/plugins/lisa/rules/reference/deployed-state-readback.md +52 -0
  70. package/plugins/lisa/rules/reference/derived-branch-plan.md +52 -0
  71. package/plugins/lisa/rules/reference/design-source-of-truth.md +43 -0
  72. package/plugins/lisa/rules/reference/design-value-binding.md +103 -0
  73. package/plugins/lisa/rules/reference/documentation-source-paths.md +18 -0
  74. package/plugins/lisa/rules/reference/leaf-only-lifecycle.md +75 -0
  75. package/plugins/lisa/rules/reference/observability-audit.md +20 -0
  76. package/plugins/lisa/rules/reference/prd-definition-of-ready.md +37 -0
  77. package/plugins/lisa/rules/reference/prd-lifecycle-rollup.md +36 -0
  78. package/plugins/lisa/rules/reference/pre-flight-autofill.md +88 -0
  79. package/plugins/lisa/rules/reference/promotion-contract.md +21 -0
  80. package/plugins/lisa/rules/reference/readiness-rubric.md +63 -0
  81. package/plugins/lisa/rules/reference/ready-role-filing.md +34 -0
  82. package/plugins/lisa/rules/reference/rejection-detection.md +69 -0
  83. package/plugins/lisa/rules/reference/repo-scope-split.md +48 -0
  84. package/plugins/lisa/rules/reference/reset-seed-coverage.md +55 -0
  85. package/plugins/lisa/rules/reference/usage-accounting.md +38 -0
  86. package/plugins/lisa/rules/reference/work-item-definition-of-ready.md +32 -0
  87. package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +158 -1
  88. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  89. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +4 -0
  90. package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +4 -0
  91. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +4 -0
  92. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +4 -1
  93. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +89 -10
  94. package/plugins/lisa-agy/hooks/parity-safety-net.sh +44 -0
  95. package/plugins/lisa-agy/plugin.json +1 -1
  96. package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +158 -1
  97. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  98. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +4 -0
  99. package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +4 -0
  100. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +4 -0
  101. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +4 -1
  102. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  104. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  105. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +89 -10
  109. package/plugins/lisa-copilot/hooks/discharge-work-item-gates.sh +7 -1
  110. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +44 -0
  111. package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +12 -0
  112. package/plugins/lisa-copilot/rules/eager/00-rule-index.md +79 -0
  113. package/plugins/lisa-copilot/rules/eager/base-rules.md +10 -0
  114. package/plugins/lisa-copilot/rules/eager/config-resolution.md +1 -88
  115. package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +97 -0
  116. package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +56 -0
  117. package/plugins/lisa-copilot/rules/reference/blocker-containment.md +58 -0
  118. package/plugins/lisa-copilot/rules/reference/claim-archaeology.md +42 -0
  119. package/plugins/lisa-copilot/rules/reference/claim-evidence-mapping.md +83 -0
  120. package/plugins/lisa-copilot/rules/reference/claim-time-guards.md +37 -0
  121. package/plugins/lisa-copilot/rules/reference/config-resolution.md +97 -0
  122. package/plugins/lisa-copilot/rules/reference/control-reachability.md +44 -0
  123. package/plugins/lisa-copilot/rules/reference/convergent-review.md +31 -0
  124. package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +57 -0
  125. package/plugins/lisa-copilot/rules/reference/dependency-decision-records.md +37 -0
  126. package/plugins/lisa-copilot/rules/reference/dependency-internalization-kit.md +47 -0
  127. package/plugins/lisa-copilot/rules/reference/dependency-trust-classes.md +48 -0
  128. package/plugins/lisa-copilot/rules/reference/deployed-state-readback.md +52 -0
  129. package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +52 -0
  130. package/plugins/lisa-copilot/rules/reference/design-source-of-truth.md +43 -0
  131. package/plugins/lisa-copilot/rules/reference/design-value-binding.md +103 -0
  132. package/plugins/lisa-copilot/rules/reference/documentation-source-paths.md +18 -0
  133. package/plugins/lisa-copilot/rules/reference/leaf-only-lifecycle.md +75 -0
  134. package/plugins/lisa-copilot/rules/reference/observability-audit.md +20 -0
  135. package/plugins/lisa-copilot/rules/reference/prd-definition-of-ready.md +37 -0
  136. package/plugins/lisa-copilot/rules/reference/prd-lifecycle-rollup.md +36 -0
  137. package/plugins/lisa-copilot/rules/reference/pre-flight-autofill.md +88 -0
  138. package/plugins/lisa-copilot/rules/reference/promotion-contract.md +21 -0
  139. package/plugins/lisa-copilot/rules/reference/readiness-rubric.md +63 -0
  140. package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +34 -0
  141. package/plugins/lisa-copilot/rules/reference/rejection-detection.md +69 -0
  142. package/plugins/lisa-copilot/rules/reference/repo-scope-split.md +48 -0
  143. package/plugins/lisa-copilot/rules/reference/reset-seed-coverage.md +55 -0
  144. package/plugins/lisa-copilot/rules/reference/usage-accounting.md +38 -0
  145. package/plugins/lisa-copilot/rules/reference/work-item-definition-of-ready.md +32 -0
  146. package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +158 -1
  147. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  148. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +4 -0
  149. package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +4 -0
  150. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +4 -0
  151. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +4 -1
  152. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +89 -10
  154. package/plugins/lisa-cursor/hooks/discharge-work-item-gates.sh +7 -1
  155. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +44 -0
  156. package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +12 -0
  157. package/plugins/lisa-cursor/rules/00-rule-index.mdc +84 -0
  158. package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +97 -0
  159. package/plugins/lisa-cursor/rules/base-rules.mdc +10 -0
  160. package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +56 -0
  161. package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +58 -0
  162. package/plugins/lisa-cursor/rules/claim-archaeology-reference.mdc +42 -0
  163. package/plugins/lisa-cursor/rules/claim-evidence-mapping-reference.mdc +83 -0
  164. package/plugins/lisa-cursor/rules/claim-time-guards-reference.mdc +37 -0
  165. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +97 -0
  166. package/plugins/lisa-cursor/rules/config-resolution.mdc +1 -88
  167. package/plugins/lisa-cursor/rules/control-reachability-reference.mdc +44 -0
  168. package/plugins/lisa-cursor/rules/convergent-review-reference.mdc +31 -0
  169. package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +57 -0
  170. package/plugins/lisa-cursor/rules/dependency-decision-records-reference.mdc +37 -0
  171. package/plugins/lisa-cursor/rules/dependency-internalization-kit-reference.mdc +47 -0
  172. package/plugins/lisa-cursor/rules/dependency-trust-classes-reference.mdc +48 -0
  173. package/plugins/lisa-cursor/rules/deployed-state-readback-reference.mdc +52 -0
  174. package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +52 -0
  175. package/plugins/lisa-cursor/rules/design-source-of-truth-reference.mdc +43 -0
  176. package/plugins/lisa-cursor/rules/design-value-binding-reference.mdc +103 -0
  177. package/plugins/lisa-cursor/rules/documentation-source-paths-reference.mdc +18 -0
  178. package/plugins/lisa-cursor/rules/leaf-only-lifecycle-reference.mdc +75 -0
  179. package/plugins/lisa-cursor/rules/observability-audit-reference.mdc +20 -0
  180. package/plugins/lisa-cursor/rules/prd-definition-of-ready-reference.mdc +37 -0
  181. package/plugins/lisa-cursor/rules/prd-lifecycle-rollup-reference.mdc +36 -0
  182. package/plugins/lisa-cursor/rules/pre-flight-autofill-reference.mdc +88 -0
  183. package/plugins/lisa-cursor/rules/promotion-contract-reference.mdc +21 -0
  184. package/plugins/lisa-cursor/rules/readiness-rubric-reference.mdc +63 -0
  185. package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +34 -0
  186. package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +69 -0
  187. package/plugins/lisa-cursor/rules/repo-scope-split-reference.mdc +48 -0
  188. package/plugins/lisa-cursor/rules/reset-seed-coverage-reference.mdc +55 -0
  189. package/plugins/lisa-cursor/rules/usage-accounting-reference.mdc +38 -0
  190. package/plugins/lisa-cursor/rules/work-item-definition-of-ready-reference.mdc +32 -0
  191. package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +158 -1
  192. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  193. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +4 -0
  194. package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +4 -0
  195. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +4 -0
  196. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +4 -1
  197. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  198. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  199. package/plugins/lisa-expo-agy/plugin.json +1 -1
  200. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  201. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  202. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  203. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  204. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  205. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  206. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  207. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  208. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  209. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  210. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  211. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  212. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  213. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  214. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  215. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  216. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  217. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  218. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  219. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  220. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  221. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  222. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  223. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  224. package/plugins/lisa-rails-agy/plugin.json +1 -1
  225. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  226. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  227. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  228. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  229. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  230. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  231. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  232. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  233. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  234. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  235. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  236. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  237. package/plugins/src/base/hooks/block-direct-issue-create.sh +89 -10
  238. package/plugins/src/base/hooks/discharge-work-item-gates.sh +7 -1
  239. package/plugins/src/base/hooks/parity-safety-net.sh +44 -0
  240. package/plugins/src/base/hooks/threshold-ratchet-families.mjs +12 -0
  241. package/plugins/src/base/rules/eager/00-rule-index.md +79 -0
  242. package/plugins/src/base/rules/eager/base-rules.md +10 -0
  243. package/plugins/src/base/rules/eager/config-resolution.md +1 -88
  244. package/plugins/src/base/rules/reference/automation-runbook-contract.md +97 -0
  245. package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +56 -0
  246. package/plugins/src/base/rules/reference/blocker-containment.md +58 -0
  247. package/plugins/src/base/rules/reference/claim-archaeology.md +42 -0
  248. package/plugins/src/base/rules/reference/claim-evidence-mapping.md +83 -0
  249. package/plugins/src/base/rules/reference/claim-time-guards.md +37 -0
  250. package/plugins/src/base/rules/reference/config-resolution.md +97 -0
  251. package/plugins/src/base/rules/reference/control-reachability.md +44 -0
  252. package/plugins/src/base/rules/reference/convergent-review.md +31 -0
  253. package/plugins/src/base/rules/reference/credential-substrate-precedence.md +57 -0
  254. package/plugins/src/base/rules/reference/dependency-decision-records.md +37 -0
  255. package/plugins/src/base/rules/reference/dependency-internalization-kit.md +47 -0
  256. package/plugins/src/base/rules/reference/dependency-trust-classes.md +48 -0
  257. package/plugins/src/base/rules/reference/deployed-state-readback.md +52 -0
  258. package/plugins/src/base/rules/reference/derived-branch-plan.md +52 -0
  259. package/plugins/src/base/rules/reference/design-source-of-truth.md +43 -0
  260. package/plugins/src/base/rules/reference/design-value-binding.md +103 -0
  261. package/plugins/src/base/rules/reference/documentation-source-paths.md +18 -0
  262. package/plugins/src/base/rules/reference/leaf-only-lifecycle.md +75 -0
  263. package/plugins/src/base/rules/reference/observability-audit.md +20 -0
  264. package/plugins/src/base/rules/reference/prd-definition-of-ready.md +37 -0
  265. package/plugins/src/base/rules/reference/prd-lifecycle-rollup.md +36 -0
  266. package/plugins/src/base/rules/reference/pre-flight-autofill.md +88 -0
  267. package/plugins/src/base/rules/reference/promotion-contract.md +21 -0
  268. package/plugins/src/base/rules/reference/readiness-rubric.md +63 -0
  269. package/plugins/src/base/rules/reference/ready-role-filing.md +34 -0
  270. package/plugins/src/base/rules/reference/rejection-detection.md +69 -0
  271. package/plugins/src/base/rules/reference/repo-scope-split.md +48 -0
  272. package/plugins/src/base/rules/reference/reset-seed-coverage.md +55 -0
  273. package/plugins/src/base/rules/reference/usage-accounting.md +38 -0
  274. package/plugins/src/base/rules/reference/work-item-definition-of-ready.md +32 -0
  275. package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +158 -1
  276. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  277. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +4 -0
  278. package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +4 -0
  279. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +4 -0
  280. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +4 -1
  281. package/rails/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
  282. package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +12 -0
  283. package/scripts/check-eager-rule-budget.mjs +176 -0
  284. package/scripts/check-generated-artifact-merge-coverage.mjs +7 -0
  285. package/scripts/check-rules-pairing.sh +45 -14
  286. package/scripts/check-shipped-surface-removals.mjs +851 -0
  287. package/scripts/cleanup-worktrees.sh +69 -16
  288. package/scripts/generate-export-surface.mjs +318 -0
  289. package/scripts/lib/shipped-surface.mjs +212 -0
  290. package/scripts/lisa-enforcement-fallback.sh +178 -1
  291. package/scripts/remedy-conformance.mjs +435 -0
  292. package/scripts/two-channel-couplings.json +2 -2
  293. package/typescript/copy-contents/.husky/pre-push +147 -2
  294. package/typescript/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
  295. package/typescript/copy-overwrite/audit.ignore.config.json +0 -10
  296. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +180 -29
  297. package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +12 -0
  298. package/typescript/package-lisa/package.lisa.json +72 -42
  299. package/plugins/lisa/rules/eager/automation-runbook-contract.md +0 -92
  300. package/plugins/lisa/rules/eager/bdd-e2e-coverage.md +0 -51
  301. package/plugins/lisa/rules/eager/blocker-containment.md +0 -53
  302. package/plugins/lisa/rules/eager/claim-archaeology.md +0 -37
  303. package/plugins/lisa/rules/eager/claim-evidence-mapping.md +0 -78
  304. package/plugins/lisa/rules/eager/claim-time-guards.md +0 -32
  305. package/plugins/lisa/rules/eager/control-reachability.md +0 -39
  306. package/plugins/lisa/rules/eager/convergent-review.md +0 -24
  307. package/plugins/lisa/rules/eager/credential-substrate-precedence.md +0 -54
  308. package/plugins/lisa/rules/eager/dependency-decision-records.md +0 -32
  309. package/plugins/lisa/rules/eager/dependency-internalization-kit.md +0 -42
  310. package/plugins/lisa/rules/eager/dependency-trust-classes.md +0 -43
  311. package/plugins/lisa/rules/eager/deployed-state-readback.md +0 -47
  312. package/plugins/lisa/rules/eager/derived-branch-plan.md +0 -47
  313. package/plugins/lisa/rules/eager/design-source-of-truth.md +0 -38
  314. package/plugins/lisa/rules/eager/design-value-binding.md +0 -98
  315. package/plugins/lisa/rules/eager/documentation-source-paths.md +0 -13
  316. package/plugins/lisa/rules/eager/factory-model.md +0 -41
  317. package/plugins/lisa/rules/eager/leaf-only-lifecycle.md +0 -70
  318. package/plugins/lisa/rules/eager/observability-audit.md +0 -15
  319. package/plugins/lisa/rules/eager/prd-definition-of-ready.md +0 -32
  320. package/plugins/lisa/rules/eager/prd-lifecycle-rollup.md +0 -31
  321. package/plugins/lisa/rules/eager/pre-flight-autofill.md +0 -84
  322. package/plugins/lisa/rules/eager/project-learnings.md +0 -22
  323. package/plugins/lisa/rules/eager/promotion-contract.md +0 -18
  324. package/plugins/lisa/rules/eager/readiness-rubric.md +0 -58
  325. package/plugins/lisa/rules/eager/ready-role-filing.md +0 -29
  326. package/plugins/lisa/rules/eager/rejection-detection.md +0 -64
  327. package/plugins/lisa/rules/eager/repo-scope-split.md +0 -43
  328. package/plugins/lisa/rules/eager/reset-seed-coverage.md +0 -50
  329. package/plugins/lisa/rules/eager/usage-accounting.md +0 -33
  330. package/plugins/lisa/rules/eager/work-item-definition-of-ready.md +0 -27
  331. package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +0 -92
  332. package/plugins/lisa-copilot/rules/eager/bdd-e2e-coverage.md +0 -51
  333. package/plugins/lisa-copilot/rules/eager/blocker-containment.md +0 -53
  334. package/plugins/lisa-copilot/rules/eager/claim-archaeology.md +0 -37
  335. package/plugins/lisa-copilot/rules/eager/claim-evidence-mapping.md +0 -78
  336. package/plugins/lisa-copilot/rules/eager/claim-time-guards.md +0 -32
  337. package/plugins/lisa-copilot/rules/eager/control-reachability.md +0 -39
  338. package/plugins/lisa-copilot/rules/eager/convergent-review.md +0 -24
  339. package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +0 -54
  340. package/plugins/lisa-copilot/rules/eager/dependency-decision-records.md +0 -32
  341. package/plugins/lisa-copilot/rules/eager/dependency-internalization-kit.md +0 -42
  342. package/plugins/lisa-copilot/rules/eager/dependency-trust-classes.md +0 -43
  343. package/plugins/lisa-copilot/rules/eager/deployed-state-readback.md +0 -47
  344. package/plugins/lisa-copilot/rules/eager/derived-branch-plan.md +0 -47
  345. package/plugins/lisa-copilot/rules/eager/design-source-of-truth.md +0 -38
  346. package/plugins/lisa-copilot/rules/eager/design-value-binding.md +0 -98
  347. package/plugins/lisa-copilot/rules/eager/documentation-source-paths.md +0 -13
  348. package/plugins/lisa-copilot/rules/eager/factory-model.md +0 -41
  349. package/plugins/lisa-copilot/rules/eager/leaf-only-lifecycle.md +0 -70
  350. package/plugins/lisa-copilot/rules/eager/observability-audit.md +0 -15
  351. package/plugins/lisa-copilot/rules/eager/prd-definition-of-ready.md +0 -32
  352. package/plugins/lisa-copilot/rules/eager/prd-lifecycle-rollup.md +0 -31
  353. package/plugins/lisa-copilot/rules/eager/pre-flight-autofill.md +0 -84
  354. package/plugins/lisa-copilot/rules/eager/project-learnings.md +0 -22
  355. package/plugins/lisa-copilot/rules/eager/promotion-contract.md +0 -18
  356. package/plugins/lisa-copilot/rules/eager/readiness-rubric.md +0 -58
  357. package/plugins/lisa-copilot/rules/eager/ready-role-filing.md +0 -29
  358. package/plugins/lisa-copilot/rules/eager/rejection-detection.md +0 -64
  359. package/plugins/lisa-copilot/rules/eager/repo-scope-split.md +0 -43
  360. package/plugins/lisa-copilot/rules/eager/reset-seed-coverage.md +0 -50
  361. package/plugins/lisa-copilot/rules/eager/usage-accounting.md +0 -33
  362. package/plugins/lisa-copilot/rules/eager/work-item-definition-of-ready.md +0 -27
  363. package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +0 -97
  364. package/plugins/lisa-cursor/rules/bdd-e2e-coverage.mdc +0 -56
  365. package/plugins/lisa-cursor/rules/blocker-containment.mdc +0 -58
  366. package/plugins/lisa-cursor/rules/claim-archaeology.mdc +0 -42
  367. package/plugins/lisa-cursor/rules/claim-evidence-mapping.mdc +0 -83
  368. package/plugins/lisa-cursor/rules/claim-time-guards.mdc +0 -37
  369. package/plugins/lisa-cursor/rules/control-reachability.mdc +0 -44
  370. package/plugins/lisa-cursor/rules/convergent-review.mdc +0 -29
  371. package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +0 -59
  372. package/plugins/lisa-cursor/rules/dependency-decision-records.mdc +0 -37
  373. package/plugins/lisa-cursor/rules/dependency-internalization-kit.mdc +0 -47
  374. package/plugins/lisa-cursor/rules/dependency-trust-classes.mdc +0 -48
  375. package/plugins/lisa-cursor/rules/deployed-state-readback.mdc +0 -52
  376. package/plugins/lisa-cursor/rules/derived-branch-plan.mdc +0 -52
  377. package/plugins/lisa-cursor/rules/design-source-of-truth.mdc +0 -43
  378. package/plugins/lisa-cursor/rules/design-value-binding.mdc +0 -103
  379. package/plugins/lisa-cursor/rules/documentation-source-paths.mdc +0 -18
  380. package/plugins/lisa-cursor/rules/factory-model.mdc +0 -46
  381. package/plugins/lisa-cursor/rules/leaf-only-lifecycle.mdc +0 -75
  382. package/plugins/lisa-cursor/rules/observability-audit.mdc +0 -20
  383. package/plugins/lisa-cursor/rules/prd-definition-of-ready.mdc +0 -37
  384. package/plugins/lisa-cursor/rules/prd-lifecycle-rollup.mdc +0 -36
  385. package/plugins/lisa-cursor/rules/pre-flight-autofill.mdc +0 -89
  386. package/plugins/lisa-cursor/rules/project-learnings.mdc +0 -27
  387. package/plugins/lisa-cursor/rules/promotion-contract.mdc +0 -23
  388. package/plugins/lisa-cursor/rules/readiness-rubric.mdc +0 -63
  389. package/plugins/lisa-cursor/rules/ready-role-filing.mdc +0 -34
  390. package/plugins/lisa-cursor/rules/rejection-detection.mdc +0 -69
  391. package/plugins/lisa-cursor/rules/repo-scope-split.mdc +0 -48
  392. package/plugins/lisa-cursor/rules/reset-seed-coverage.mdc +0 -55
  393. package/plugins/lisa-cursor/rules/usage-accounting.mdc +0 -38
  394. package/plugins/lisa-cursor/rules/work-item-definition-of-ready.mdc +0 -32
  395. package/plugins/src/base/rules/eager/automation-runbook-contract.md +0 -92
  396. package/plugins/src/base/rules/eager/bdd-e2e-coverage.md +0 -51
  397. package/plugins/src/base/rules/eager/blocker-containment.md +0 -53
  398. package/plugins/src/base/rules/eager/claim-archaeology.md +0 -37
  399. package/plugins/src/base/rules/eager/claim-evidence-mapping.md +0 -78
  400. package/plugins/src/base/rules/eager/claim-time-guards.md +0 -32
  401. package/plugins/src/base/rules/eager/control-reachability.md +0 -39
  402. package/plugins/src/base/rules/eager/convergent-review.md +0 -24
  403. package/plugins/src/base/rules/eager/credential-substrate-precedence.md +0 -54
  404. package/plugins/src/base/rules/eager/dependency-decision-records.md +0 -32
  405. package/plugins/src/base/rules/eager/dependency-internalization-kit.md +0 -42
  406. package/plugins/src/base/rules/eager/dependency-trust-classes.md +0 -43
  407. package/plugins/src/base/rules/eager/deployed-state-readback.md +0 -47
  408. package/plugins/src/base/rules/eager/derived-branch-plan.md +0 -47
  409. package/plugins/src/base/rules/eager/design-source-of-truth.md +0 -38
  410. package/plugins/src/base/rules/eager/design-value-binding.md +0 -98
  411. package/plugins/src/base/rules/eager/documentation-source-paths.md +0 -13
  412. package/plugins/src/base/rules/eager/factory-model.md +0 -41
  413. package/plugins/src/base/rules/eager/leaf-only-lifecycle.md +0 -70
  414. package/plugins/src/base/rules/eager/observability-audit.md +0 -15
  415. package/plugins/src/base/rules/eager/prd-definition-of-ready.md +0 -32
  416. package/plugins/src/base/rules/eager/prd-lifecycle-rollup.md +0 -31
  417. package/plugins/src/base/rules/eager/pre-flight-autofill.md +0 -84
  418. package/plugins/src/base/rules/eager/project-learnings.md +0 -22
  419. package/plugins/src/base/rules/eager/promotion-contract.md +0 -18
  420. package/plugins/src/base/rules/eager/readiness-rubric.md +0 -58
  421. package/plugins/src/base/rules/eager/ready-role-filing.md +0 -29
  422. package/plugins/src/base/rules/eager/rejection-detection.md +0 -64
  423. package/plugins/src/base/rules/eager/repo-scope-split.md +0 -43
  424. package/plugins/src/base/rules/eager/reset-seed-coverage.md +0 -50
  425. package/plugins/src/base/rules/eager/usage-accounting.md +0 -33
  426. package/plugins/src/base/rules/eager/work-item-definition-of-ready.md +0 -27
@@ -326,6 +326,12 @@ export function run(command, args, options = {}) {
326
326
  encoding: "utf8",
327
327
  env: options.env ?? process.env,
328
328
  input: options.input,
329
+ // Node's default is 1 MiB, and it is silent in exactly the wrong way: an
330
+ // over-large stdout sets `error` while leaving `status` null, so a caller
331
+ // passing `allowFailure` and testing `status !== 0` reads "the command
332
+ // returned nothing" when what happened is "the command was cut off". A
333
+ // caller that needs more says so; the default stays.
334
+ maxBuffer: options.maxBuffer,
329
335
  timeout: options.timeout ?? CHILD_TIMEOUT_MS,
330
336
  // SIGKILL, not the default SIGTERM. `timeout` sends killSignal and then
331
337
  // keeps waiting for the child to exit, so a child that traps or ignores
@@ -1837,11 +1843,74 @@ function carriesMarker(body) {
1837
1843
  return JSON.stringify(body ?? "").includes(MARKER);
1838
1844
  }
1839
1845
 
1846
+ /**
1847
+ * Lisa's managed backlink comments, split by whether one is THIS pull request's.
1848
+ *
1849
+ * The whole defect this exists to remove is in the word "the": the writers used
1850
+ * to look for *a* managed comment and rewrite it, so a work item with a second
1851
+ * pull request against it had the first one's backlink silently repointed. The
1852
+ * earlier pull request then failed a gate it had already passed, for a reason
1853
+ * that was not in front of anyone looking at it, and the tool said `updated` —
1854
+ * which reads as "added a link", not "took someone else's away".
1855
+ *
1856
+ * `[lisa-pr-link]` is a statement about a PAIR: *this pull request is linked to
1857
+ * this item*. Storing a pair in a single slot means every new pair evicts the
1858
+ * last, so the comment is keyed on the pull request it names and a work item
1859
+ * carries one per pull request. Nothing here needs a migration: an item holding
1860
+ * one legacy comment is simply an item where `mine` matches or it does not.
1861
+ *
1862
+ * This is deliberately NOT stack-awareness. A fix and its follow-up, a revert
1863
+ * and its replacement, a pull request reopened or recreated against a different
1864
+ * base, and work split across repositories all put two pull requests on one
1865
+ * item, and none of them involve a stack.
1866
+ * @param {readonly unknown[]} comments Comments as the provider returned them.
1867
+ * @param {string} prUrl The pull request being discharged.
1868
+ * @param {(comment: unknown) => unknown} bodyOf Reads a comment's body.
1869
+ * @returns {{mine: unknown, others: number}} This PR's comment, and how many
1870
+ * managed comments belong to OTHER pull requests.
1871
+ */
1872
+ function partitionBacklinks(comments, prUrl, bodyOf) {
1873
+ const mine = [];
1874
+ let others = 0;
1875
+ for (const comment of comments) {
1876
+ const body = bodyOf(comment);
1877
+ if (!carriesMarker(body)) continue;
1878
+ if (textContainsBacklink(body, prUrl)) mine.push(comment);
1879
+ else others += 1;
1880
+ }
1881
+ return { mine: mine[0], others };
1882
+ }
1883
+
1884
+ /**
1885
+ * Say what a backlink write did, including what it did NOT touch.
1886
+ *
1887
+ * The sibling count is reported rather than left implicit because a tool that
1888
+ * writes to a shared surface should say what else is on it. `updated` was the
1889
+ * only signal the old writer gave before it evicted another pull request's
1890
+ * link, and one word is not enough to distinguish "added" from "replaced".
1891
+ * Exported so the wording can be asserted without a tracker.
1892
+ * @param {{change: string, others: number}} outcome What the writer did.
1893
+ * @param {string} ref Canonical work-item reference.
1894
+ * @param {string} prUrl Pull request URL.
1895
+ * @returns {string} The operator-readable report.
1896
+ */
1897
+ export function backlinkReport(outcome, ref, prUrl) {
1898
+ const line = `work-item backlink ${outcome.change} on ${ref}: ${MARKER} ${prUrl}`;
1899
+ if (!outcome.others) return line;
1900
+ const plural = outcome.others === 1 ? "" : "s";
1901
+ return (
1902
+ `${line}\n ${outcome.others} other pull request${plural} already linked ` +
1903
+ `to ${ref}, left untouched — an item carries one backlink per pull ` +
1904
+ `request, and discharging one never removes another.`
1905
+ );
1906
+ }
1907
+
1840
1908
  /**
1841
1909
  * Establish the backlink on a GitHub issue.
1842
1910
  * @param {string} ref Canonical `owner/repo#number` reference.
1843
1911
  * @param {string} prUrl Pull request URL.
1844
- * @returns {string} What changed: created, updated, or unchanged.
1912
+ * @returns {{change: string, others: number}} What changed, and how many other
1913
+ * pull requests are linked to this item and were left alone.
1845
1914
  */
1846
1915
  function githubBacklink(ref, prUrl) {
1847
1916
  const [repository, number] = ref.split("#");
@@ -1856,18 +1925,26 @@ function githubBacklink(ref, prUrl) {
1856
1925
  );
1857
1926
  if (listing.status !== 0) throw githubFailure(listing, ref);
1858
1927
  const comments = safeJson(listing.stdout, `GitHub comments on ${ref}`);
1859
- const managed = (Array.isArray(comments) ? comments : []).find(comment =>
1860
- carriesMarker(comment?.body)
1928
+ // This listing is the read-before-write, and it is fetched here rather than
1929
+ // passed in for exactly that reason: the version you last wrote is not the
1930
+ // version that is live on a surface other agents also comment on.
1931
+ const { mine, others } = partitionBacklinks(
1932
+ Array.isArray(comments) ? comments : [],
1933
+ prUrl,
1934
+ comment => comment?.body
1861
1935
  );
1862
1936
  const body = backlinkBody(prUrl);
1863
- if (managed && managed.body === body) return "unchanged";
1864
- const [method, endpoint] = managed
1865
- ? ["PATCH", `repos/${repository}/issues/comments/${managed.id}`]
1937
+ if (mine && mine.body === body) return { change: "unchanged", others };
1938
+ // PATCH only ever targets THIS pull request's own comment. A managed comment
1939
+ // naming a different pull request is that pull request's proof of gate 5,
1940
+ // and rewriting it would fail a gate that had already passed.
1941
+ const [method, endpoint] = mine
1942
+ ? ["PATCH", `repos/${repository}/issues/comments/${mine.id}`]
1866
1943
  : ["POST", `repos/${repository}/issues/${number}/comments`];
1867
1944
  run("gh", ["api", "--method", method, endpoint, "--field", `body=${body}`], {
1868
1945
  error: `could not write the backlink comment on ${ref}`,
1869
1946
  });
1870
- return managed ? "updated" : "created";
1947
+ return { change: mine ? "updated" : "created", others };
1871
1948
  }
1872
1949
 
1873
1950
  /**
@@ -1906,7 +1983,8 @@ function linearGraphql(token, query, variables, context) {
1906
1983
  * @param {string} ref Canonical `KEY-123` identifier.
1907
1984
  * @param {string} prUrl Pull request URL.
1908
1985
  * @param {object} contract Resolved tracker contract.
1909
- * @returns {string} What changed: created, updated, or unchanged.
1986
+ * @returns {{change: string, others: number}} What changed, and how many other
1987
+ * pull requests are linked to this item and were left alone.
1910
1988
  */
1911
1989
  function linearBacklink(ref, prUrl, contract) {
1912
1990
  const token = readLinearKey(contract.workspace);
@@ -1930,19 +2008,21 @@ function linearBacklink(ref, prUrl, contract) {
1930
2008
  throw new TrackingError(
1931
2009
  `Linear issue ${ref} does not exist or is inaccessible`
1932
2010
  );
1933
- const managed = (issue.comments?.nodes ?? []).find(comment =>
1934
- carriesMarker(comment?.body)
2011
+ const { mine, others } = partitionBacklinks(
2012
+ issue.comments?.nodes ?? [],
2013
+ prUrl,
2014
+ comment => comment?.body
1935
2015
  );
1936
2016
  const body = backlinkBody(prUrl);
1937
- if (managed && managed.body === body) return "unchanged";
1938
- if (managed) {
2017
+ if (mine && mine.body === body) return { change: "unchanged", others };
2018
+ if (mine) {
1939
2019
  linearGraphql(
1940
2020
  token,
1941
2021
  "mutation($id:String!,$body:String!){commentUpdate(id:$id,input:{body:$body}){success}}",
1942
- { body, id: managed.id },
2022
+ { body, id: mine.id },
1943
2023
  `Linear backlink update on ${ref}`
1944
2024
  );
1945
- return "updated";
2025
+ return { change: "updated", others };
1946
2026
  }
1947
2027
  linearGraphql(
1948
2028
  token,
@@ -1950,7 +2030,7 @@ function linearBacklink(ref, prUrl, contract) {
1950
2030
  { body, id: issue.id },
1951
2031
  `Linear backlink comment on ${ref}`
1952
2032
  );
1953
- return "created";
2033
+ return { change: "created", others };
1954
2034
  }
1955
2035
 
1956
2036
  /**
@@ -1985,7 +2065,8 @@ function jiraCommentDocument(prUrl) {
1985
2065
  * @param {string} ref Canonical `KEY-123` reference.
1986
2066
  * @param {string} prUrl Pull request URL.
1987
2067
  * @param {object} contract Resolved tracker contract.
1988
- * @returns {string} What changed: created, updated, or unchanged.
2068
+ * @returns {{change: string, others: number}} What changed, and how many other
2069
+ * pull requests are linked to this item and were left alone.
1989
2070
  */
1990
2071
  function jiraBacklink(ref, prUrl, contract) {
1991
2072
  const credentials = jiraCredentials(contract);
@@ -2004,24 +2085,26 @@ function jiraBacklink(ref, prUrl, contract) {
2004
2085
  if (listing.status !== 0)
2005
2086
  throw new TrackingError(`Jira ticket ${ref} comments are inaccessible`);
2006
2087
  const existing = safeJson(listing.stdout, `Jira comments on ${ref}`);
2007
- const managed = (existing.comments ?? []).find(comment =>
2008
- carriesMarker(comment?.body)
2088
+ const { mine, others } = partitionBacklinks(
2089
+ existing.comments ?? [],
2090
+ prUrl,
2091
+ comment => comment?.body
2009
2092
  );
2010
2093
  const document = jiraCommentDocument(prUrl);
2011
2094
  const payload = JSON.stringify({ body: document });
2012
- if (managed && JSON.stringify(managed.body) === JSON.stringify(document))
2013
- return "unchanged";
2095
+ if (mine && JSON.stringify(mine.body) === JSON.stringify(document))
2096
+ return { change: "unchanged", others };
2014
2097
  secureCurl(
2015
- [managed ? `${issueUrl}/${encodeURIComponent(managed.id)}` : issueUrl],
2098
+ [mine ? `${issueUrl}/${encodeURIComponent(mine.id)}` : issueUrl],
2016
2099
  [
2017
2100
  ...auth,
2018
- ["request", managed ? "PUT" : "POST"],
2101
+ ["request", mine ? "PUT" : "POST"],
2019
2102
  ["header", CONTENT_TYPE_JSON],
2020
2103
  [DATA_BINARY, payload],
2021
2104
  ],
2022
2105
  { error: `could not write the backlink comment on ${ref}` }
2023
2106
  );
2024
- return managed ? "updated" : "created";
2107
+ return { change: mine ? "updated" : "created", others };
2025
2108
  }
2026
2109
 
2027
2110
  /**
@@ -2039,7 +2122,8 @@ function jiraBacklink(ref, prUrl, contract) {
2039
2122
  * @param {string} ref Canonical work-item reference.
2040
2123
  * @param {string} prUrl Pull request URL.
2041
2124
  * @param {object} contract Resolved tracker contract.
2042
- * @returns {string} What changed: created, updated, or unchanged.
2125
+ * @returns {{change: string, others: number}} What changed, and how many other
2126
+ * pull requests are linked to this item and were left alone.
2043
2127
  */
2044
2128
  function postBacklink(ref, prUrl, contract) {
2045
2129
  if (contract.provider === "github") return githubBacklink(ref, prUrl);
@@ -2442,6 +2526,74 @@ function remoteDefaultRef(remote) {
2442
2526
  return exists.status === 0 ? target : undefined;
2443
2527
  }
2444
2528
 
2529
+ /**
2530
+ * The scope `validate-push` reports when it never saw the pushed refs at all.
2531
+ *
2532
+ * Named rather than left blank because this is the exact case #3874 measured:
2533
+ * with no refs to read, the range falls back to the PUSHER'S `HEAD` — which on
2534
+ * a cross-worktree push is a different branch than the one being pushed. The
2535
+ * answer computed there is accurate about the local checkout and says nothing
2536
+ * whatsoever about what is leaving the machine, so the verdict has to carry the
2537
+ * scope it was computed over or a reader cannot tell the two apart.
2538
+ */
2539
+ const HEAD_FALLBACK_SCOPE =
2540
+ "local HEAD (the pushed refs were not readable, so this is NOT the pushed range)";
2541
+
2542
+ /**
2543
+ * An object id, short enough to read and long enough to be unambiguous.
2544
+ * @param {string} oid A 40-character object id.
2545
+ * @returns {string} The first twelve characters.
2546
+ */
2547
+ function abbreviate(oid) {
2548
+ return oid.slice(0, 12);
2549
+ }
2550
+
2551
+ /**
2552
+ * The pushed refs, from wherever this invocation can actually reach them.
2553
+ *
2554
+ * READING THE STREAM SPENDS IT, and the pre-push hook has several readers: the
2555
+ * deletion guard, the destination guard, every declared gate, and this. The
2556
+ * hook therefore captures git's stdin to a file once and names it in
2557
+ * `LISA_PUSHED_REFS_FILE`, which is the only source that survives a gate having
2558
+ * already drained the stream — and the gate runner is exactly where this runs
2559
+ * on a project that declares `gates.traceability`.
2560
+ *
2561
+ * Stdin stays the fallback so driving the subcommand by hand with a pipe still
2562
+ * works, and an unreadable file falls through to it rather than failing: a
2563
+ * scratch file that vanished must not turn a push into an error about
2564
+ * plumbing.
2565
+ * @param {string|undefined} refsFile Path named by `--refs` or the environment.
2566
+ * @returns {string} The raw stream, possibly empty.
2567
+ */
2568
+ function readPushRefs(refsFile) {
2569
+ if (refsFile !== undefined) {
2570
+ try {
2571
+ return readFileSync(refsFile, "utf8");
2572
+ } catch {
2573
+ // Fall through to stdin below.
2574
+ }
2575
+ }
2576
+ try {
2577
+ return readFileSync(0, "utf8");
2578
+ } catch {
2579
+ return "";
2580
+ }
2581
+ }
2582
+
2583
+ /**
2584
+ * The scope clause every push verdict carries, pass or fail.
2585
+ *
2586
+ * A green that does not say what it looked at is indistinguishable from a green
2587
+ * that looked at the wrong thing, which is the whole of #3874: `0 commit(s)`
2588
+ * printed for a branch carrying a traced commit, because the range came from
2589
+ * the pusher's checkout instead of the pushed ref.
2590
+ * @param {string} scope The range or tree examined.
2591
+ * @returns {string} The clause, ready to append to a verdict line.
2592
+ */
2593
+ function examined(scope) {
2594
+ return ` [examined ${scope}]`;
2595
+ }
2596
+
2445
2597
  /**
2446
2598
  * One pushed ref and the commits it introduces, per line git sent.
2447
2599
  *
@@ -2460,7 +2612,8 @@ function remoteDefaultRef(remote) {
2460
2612
  * the rule is unchanged and now applied to the unit it was always about.
2461
2613
  * @param {string} input The pre-push stdin stream.
2462
2614
  * @param {string} remote Remote being pushed to.
2463
- * @returns {{localRef: string|undefined, commits: string[], scope: string[]}[]}
2615
+ * @returns {{localRef: string|undefined, commits: string[], scope: string[],
2616
+ * examinedScope: string}[]}
2464
2617
  * One entry per ref update that introduces commits. `scope` is the rev-list
2465
2618
  * argument vector the commit list came from, kept so a later question about
2466
2619
  * the same range — see {@link ancestryUnreachable} — is asked of the range
@@ -2475,30 +2628,34 @@ function parsePushGroups(input, remote) {
2475
2628
  for (const line of input.trim().split(/\r?\n/).filter(Boolean)) {
2476
2629
  const [localRef, localOid, , remoteOid] = line.trim().split(/\s+/);
2477
2630
  if (!localOid || ZERO_OID.test(localOid)) continue;
2478
- const args =
2479
- remoteOid && !ZERO_OID.test(remoteOid)
2480
- ? [
2481
- "rev-list",
2482
- `${remoteOid}..${localOid}`,
2483
- ...(defaultRef ? ["--not", defaultRef] : []),
2484
- ]
2485
- : // New-branch lane: `--remotes=<remote>` is safe HERE because the branch
2486
- // being pushed has no remote-tracking ref yet — the exclusion set can
2487
- // only contain refs that already passed validation on earlier pushes.
2488
- // The existing-branch lane above must NOT use it (its tracking ref is
2489
- // pusher-controlled); it excludes only the remote default branch.
2490
- ["rev-list", localOid, "--not", `--remotes=${remote}`];
2631
+ const existing = Boolean(remoteOid) && !ZERO_OID.test(remoteOid);
2632
+ const args = existing
2633
+ ? [
2634
+ "rev-list",
2635
+ `${remoteOid}..${localOid}`,
2636
+ ...(defaultRef ? ["--not", defaultRef] : []),
2637
+ ]
2638
+ : // New-branch lane: `--remotes=<remote>` is safe HERE because the branch
2639
+ // being pushed has no remote-tracking ref yet — the exclusion set can
2640
+ // only contain refs that already passed validation on earlier pushes.
2641
+ // The existing-branch lane above must NOT use it (its tracking ref is
2642
+ // pusher-controlled); it excludes only the remote default branch.
2643
+ ["rev-list", localOid, "--not", `--remotes=${remote}`];
2491
2644
  groups.push({
2492
- localRef,
2493
2645
  commits: git(args).split("\n").filter(Boolean),
2646
+ examinedScope: existing
2647
+ ? `${localRef} ${abbreviate(remoteOid)}..${abbreviate(localOid)}`
2648
+ : `${localRef} (new branch) ${abbreviate(localOid)}`,
2649
+ localRef,
2494
2650
  scope: args,
2495
2651
  });
2496
2652
  }
2497
2653
  if (groups.length === 0 && input.trim() === "") {
2498
2654
  const args = ["rev-list", "HEAD", "--not", `--remotes=${remote}`];
2499
2655
  groups.push({
2500
- localRef: undefined,
2501
2656
  commits: git(args).split("\n").filter(Boolean),
2657
+ examinedScope: HEAD_FALLBACK_SCOPE,
2658
+ localRef: undefined,
2502
2659
  scope: args,
2503
2660
  });
2504
2661
  }
@@ -2783,6 +2940,14 @@ function collect(findings, scope, gate, check) {
2783
2940
  * ticket id in it can never acquire one — that requirement is unreachable from
2784
2941
  * inside the pull request and the message has to say so, rather than leaving a
2785
2942
  * reader to try edits that cannot work.
2943
+ *
2944
+ * The closing sentence used to read "creates that comment or updates the
2945
+ * existing one", and that was the whole problem: the existing one could belong
2946
+ * to a DIFFERENT pull request, so following this printed remedy broke a pull
2947
+ * request that had done nothing wrong (CodySwannGT/lisa#3916). The remedy is
2948
+ * now safe by construction rather than by the reader noticing, and it says so —
2949
+ * a reader who has just been told a gate is unmet needs to know that running
2950
+ * the fix cannot un-fix somebody else.
2786
2951
  * @param {string} ref Canonical work-item reference.
2787
2952
  * @param {string} prUrl Pull request URL.
2788
2953
  * @param {object} contract Resolved tracker contract.
@@ -2797,7 +2962,10 @@ function backlinkAdvice(ref, prUrl, contract) {
2797
2962
  `${branchDerived}The one remedy that needs no new branch is the managed comment ` +
2798
2963
  `\`${MARKER} ${prUrl}\` on ${ref}. Do not post it by hand — run:\n\n` +
2799
2964
  ` ${BACKLINK_COMMAND} --ref ${ref} --pr-url ${prUrl}\n\n` +
2800
- `which creates that comment or updates the existing one, so running it twice is safe`
2965
+ `which creates that comment, or updates the one already naming THIS pull ` +
2966
+ `request. A backlink belonging to another pull request on ${ref} is left ` +
2967
+ `alone, so running this twice — or for a second pull request against the ` +
2968
+ `same item — is safe`
2801
2969
  );
2802
2970
  }
2803
2971
 
@@ -3446,8 +3614,72 @@ function backlink(args) {
3446
3614
  const prUrl = pullRequestUrlOption(args);
3447
3615
  if (!prUrl)
3448
3616
  throw new TrackingError(`${BACKLINK_COMMAND} requires --pr-url <url>`);
3449
- const outcome = postBacklink(ref, prUrl, contract);
3450
- console.log(`work-item backlink ${outcome} on ${ref}: ${MARKER} ${prUrl}`);
3617
+ console.log(backlinkReport(postBacklink(ref, prUrl, contract), ref, prUrl));
3618
+ }
3619
+
3620
+ /**
3621
+ * Every work item a commit body DECLARES, by full-body scan.
3622
+ *
3623
+ * A declaration is the commit's own `Work-Item:` line. It is written by the
3624
+ * author of the work, enforced on every non-merge commit by the commit-msg
3625
+ * gate, and says "this commit is that item". A cross-reference says only that
3626
+ * somebody's pull request NAMED the item somewhere, which is a different and
3627
+ * much weaker claim — see `sweepDeclarations` for what that difference cost.
3628
+ *
3629
+ * Scanned with the `m` flag over the WHOLE body rather than read through git's
3630
+ * `%(trailers:key=Work-Item)`, and that is the load-bearing part. Git's trailer
3631
+ * parser only considers the LAST paragraph of a message, and this project's
3632
+ * commits put `Work-Item:` above the trailing co-author block, so the parser
3633
+ * does not see it as a trailer at all. Measured on `origin/main`: the parser
3634
+ * finds 602 distinct issues where a full-body scan finds 835 — it misses 233,
3635
+ * and the undercount is clean, plausible and well-formed, which is why it
3636
+ * survived. Anything in this fleet that counts or verifies trailers has to
3637
+ * scan the body (CodySwannGT/lisa#3859).
3638
+ * @param {string} body A commit message body.
3639
+ * @param {string} repository `owner/name` the item must belong to.
3640
+ * @returns {number[]} Declared issue numbers, de-duplicated, in first-seen order.
3641
+ */
3642
+ export function declaredWorkItemNumbers(body, repository) {
3643
+ const escaped = String(repository ?? "").replace(
3644
+ /[.*+?^${}()|[\]\\]/g,
3645
+ "\\$&"
3646
+ );
3647
+ if (!escaped) return [];
3648
+ const pattern = new RegExp(
3649
+ `^[ \\t]*Work-Item:[ \\t]*${escaped}#([1-9]\\d*)[ \\t]*$`,
3650
+ "gim"
3651
+ );
3652
+ const numbers = [];
3653
+ let match;
3654
+ while ((match = pattern.exec(String(body ?? ""))) !== null)
3655
+ numbers.push(Number(match[1]));
3656
+ return [...new Set(numbers)];
3657
+ }
3658
+
3659
+ /**
3660
+ * Work items declared by commits in a `git log` transcript.
3661
+ *
3662
+ * Pure over the transcript text so the whole attribution can be asserted
3663
+ * without a repository. Records are NUL-separated (`git log -z`) because a
3664
+ * commit body contains newlines and blank lines by construction, and any
3665
+ * newline-based framing would split one commit into several.
3666
+ * @param {string} logText Output of `git log -z --format=%H%n%B`.
3667
+ * @param {string} repository `owner/name` the item must belong to.
3668
+ * @returns {Map<number, string[]>} Issue number to the commits declaring it.
3669
+ */
3670
+ export function shippedDeclarations(logText, repository) {
3671
+ const declarations = new Map();
3672
+ for (const record of String(logText ?? "").split("\0")) {
3673
+ if (record.trim() === "") continue;
3674
+ const newline = record.indexOf("\n");
3675
+ const sha = (newline === -1 ? record : record.slice(0, newline)).trim();
3676
+ const body = newline === -1 ? "" : record.slice(newline + 1);
3677
+ for (const number of declaredWorkItemNumbers(body, repository)) {
3678
+ if (!declarations.has(number)) declarations.set(number, []);
3679
+ declarations.get(number).push(sha);
3680
+ }
3681
+ }
3682
+ return declarations;
3451
3683
  }
3452
3684
 
3453
3685
  /**
@@ -4210,17 +4442,42 @@ function complete(args) {
4210
4442
  * Reports by default and only acts under `--apply`, because a sweep that closes
4211
4443
  * things as a side effect of being run is not something anyone will run twice.
4212
4444
  */
4213
- function sweep(args) {
4214
- const contract = trackerContract();
4215
- if (contract.provider !== "github") {
4216
- throw new TrackingError(
4217
- `no sweep for tracker '${contract.provider}'; only github is supported so far.`
4218
- );
4219
- }
4220
- const repository = currentRepository();
4221
- if (!repository)
4222
- throw new TrackingError("could not resolve the current GitHub repository");
4223
- const claimed = contract.lifecycle.claimed;
4445
+ /**
4446
+ * Buffer bound for a deploy branch's commit log.
4447
+ *
4448
+ * Sized against measurement rather than taste: `origin/main` here is 4.6 MiB
4449
+ * of `%H%n%B` across ~1,600 commits, so 64 MiB leaves an order of magnitude of
4450
+ * headroom and still bounds a runaway. It is a ceiling, not a target — and
4451
+ * exceeding it now refuses loudly instead of reading as "no drift".
4452
+ */
4453
+ const DECLARATION_LOG_MAX_BYTES = 64 * 1024 * 1024;
4454
+
4455
+ /**
4456
+ * The lifecycle roles the sweep examines, in report order.
4457
+ *
4458
+ * Both open lanes, not just the claimed one. An item whose work shipped while
4459
+ * it still carried the ready role is structurally invisible to a claimed-only
4460
+ * subject list — the flow never claimed it, or the claim was reverted, or the
4461
+ * pull request came from a session that never transitioned it. Measured on
4462
+ * this repository: of 122 open ready items, 11 had commits on `origin/main`
4463
+ * carrying their `Work-Item:` trailer, and the sweep reported none of them in
4464
+ * either mode (CodySwannGT/lisa#3907).
4465
+ * @param {object} lifecycle Resolved lifecycle contract.
4466
+ * @returns {string[]} Role names to query, de-duplicated.
4467
+ */
4468
+ function sweptRoles(lifecycle) {
4469
+ return [...new Set([lifecycle.ready, lifecycle.claimed])].filter(
4470
+ role => typeof role === "string" && role !== ""
4471
+ );
4472
+ }
4473
+
4474
+ /**
4475
+ * Open issues carrying one lifecycle role.
4476
+ * @param {string} repository `owner/name` to list from.
4477
+ * @param {string} role The label to filter by.
4478
+ * @returns {object[]} Issues with `number` and `title`.
4479
+ */
4480
+ function openIssuesWithRole(repository, role) {
4224
4481
  const listing = run(
4225
4482
  "gh",
4226
4483
  [
@@ -4231,7 +4488,7 @@ function sweep(args) {
4231
4488
  "--state",
4232
4489
  "open",
4233
4490
  "--label",
4234
- claimed,
4491
+ role,
4235
4492
  "--limit",
4236
4493
  "200",
4237
4494
  "--json",
@@ -4241,33 +4498,157 @@ function sweep(args) {
4241
4498
  );
4242
4499
  if (listing.status !== 0) throw githubFailure(listing, repository);
4243
4500
  const issues = safeJson(listing.stdout, "GitHub issue listing");
4501
+ return Array.isArray(issues) ? issues : [];
4502
+ }
4503
+
4504
+ /**
4505
+ * The first of `origin/<branch>` and `<branch>` that resolves to a commit.
4506
+ * @param {string} branch Configured deploy branch name.
4507
+ * @returns {string|undefined} A resolvable rev, or undefined.
4508
+ */
4509
+ function resolvedBranchRev(branch) {
4510
+ for (const candidate of [`origin/${branch}`, branch]) {
4511
+ const result = run(
4512
+ "git",
4513
+ ["rev-parse", "--verify", "--quiet", `${candidate}^{commit}`],
4514
+ { allowFailure: true }
4515
+ );
4516
+ if (result.status === 0 && result.stdout.trim() !== "") return candidate;
4517
+ }
4518
+ return undefined;
4519
+ }
4520
+
4521
+ /**
4522
+ * Work items DECLARED by commits reachable from a deploy branch.
4523
+ *
4524
+ * This is the sweep's evidence, and it replaced a timeline cross-reference
4525
+ * scan. The difference is not academic. A cross-reference is created by any
4526
+ * pull request that so much as NAMES `#N` — so one merged land-stack pull
4527
+ * request made every sibling in its batch look shipped, and a single pull
4528
+ * request was counted as evidence for several unrelated items at once.
4529
+ * Measured before this change: of 53 items the sweep reported as drifted, 8
4530
+ * had no commit on `main` declaring them at all — 15%, and every one of the 8
4531
+ * had a live worktree branch open against it. Applying the sweep would have
4532
+ * closed eight in-flight work items, and by this file's own words the closure
4533
+ * "would be indistinguishable from a real one afterwards".
4534
+ *
4535
+ * Reachability is also what confirms the work actually landed, rather than
4536
+ * trusting a pull request's `baseRefName` to say where it went.
4537
+ * @param {string} repository `owner/name` the items belong to.
4538
+ * @param {object} contract Resolved tracker contract.
4539
+ * @returns {Map<number, string[]>} Issue number to declaring commits.
4540
+ */
4541
+ function deployedDeclarations(repository, contract) {
4542
+ const declarations = new Map();
4543
+ for (const branch of contract.deployBranches.keys()) {
4544
+ const rev = resolvedBranchRev(branch);
4545
+ if (!rev) continue;
4546
+ const result = run("git", ["log", rev, "-z", "--format=%H%n%B"], {
4547
+ allowFailure: true,
4548
+ maxBuffer: DECLARATION_LOG_MAX_BYTES,
4549
+ });
4550
+ // Loudly, not `continue`. This sweep's whole output is an absence claim,
4551
+ // and a read that failed is not an absence — under the default 1 MiB
4552
+ // buffer the real `origin/main` log (4.6 MiB) was truncated into an
4553
+ // `error` with a null status, and the run reported "No drift" over 194
4554
+ // items while having read nothing. Refusing is the only safe answer,
4555
+ // because the alternative is the tool's most reassuring output being its
4556
+ // least trustworthy one.
4557
+ if (result.error || result.status !== 0) {
4558
+ throw new TrackingError(
4559
+ `could not read the commit log of ${rev}, so no absence of drift can be reported: ` +
4560
+ `${result.error ? result.error.message : result.stderr.trim() || "git log failed"}.\n` +
4561
+ `The sweep reports what a deploy branch declares; a log it could not read is not an empty one.`
4562
+ );
4563
+ }
4564
+ for (const [number, shas] of shippedDeclarations(
4565
+ result.stdout,
4566
+ repository
4567
+ )) {
4568
+ declarations.set(number, [
4569
+ ...new Set([...(declarations.get(number) ?? []), ...shas]),
4570
+ ]);
4571
+ }
4572
+ }
4573
+ return declarations;
4574
+ }
4575
+
4576
+ /**
4577
+ * Render the commits that declare an item, without printing a wall of hashes.
4578
+ * @param {string[]} shas Declaring commits.
4579
+ * @returns {string} A short, readable citation.
4580
+ */
4581
+ function describeDeclarations(shas) {
4582
+ const shown = shas.slice(0, 3).map(sha => sha.slice(0, 9));
4583
+ const rest = shas.length - shown.length;
4584
+ return rest > 0 ? `${shown.join(", ")}, +${rest} more` : shown.join(", ");
4585
+ }
4586
+
4587
+ function sweep(args) {
4588
+ const contract = trackerContract();
4589
+ if (contract.provider !== "github") {
4590
+ throw new TrackingError(
4591
+ `no sweep for tracker '${contract.provider}'; only github is supported so far.`
4592
+ );
4593
+ }
4594
+ const repository = currentRepository();
4595
+ if (!repository)
4596
+ throw new TrackingError("could not resolve the current GitHub repository");
4597
+ const roles = sweptRoles(contract.lifecycle);
4598
+ const subjects = new Map();
4599
+ for (const role of roles) {
4600
+ for (const issue of openIssuesWithRole(repository, role)) {
4601
+ const existing = subjects.get(issue.number);
4602
+ if (existing) existing.roles.push(role);
4603
+ else subjects.set(issue.number, { ...issue, roles: [role] });
4604
+ }
4605
+ }
4606
+ const declarations = deployedDeclarations(repository, contract);
4244
4607
  const apply = args.includes("--apply");
4608
+ // Named in every report, clean or not. The old clean-result sentence spoke
4609
+ // only of the claimed role while the subject list excluded the ready lane
4610
+ // entirely — a true sentence that answered a narrower question than the one
4611
+ // its reader was asking. An instrument that reports an absence has to say
4612
+ // what it looked at.
4613
+ const examined = roles.map(role => `"${role}"`).join(" and ");
4245
4614
  let drifted = 0;
4246
- for (const issue of Array.isArray(issues) ? issues : []) {
4247
- const ref = `${repository}#${issue.number}`;
4248
- const merged = mergedPullRequestsIn(githubTimeline(ref), repository);
4249
- if (merged.length === 0) continue;
4615
+ for (const issue of [...subjects.values()].sort(
4616
+ (left, right) => right.number - left.number
4617
+ )) {
4618
+ const shas = declarations.get(issue.number) ?? [];
4619
+ if (shas.length === 0) continue;
4250
4620
  drifted += 1;
4251
- const evidence = merged.map(number => `#${number}`).join(", ");
4621
+ const ref = `${repository}#${issue.number}`;
4252
4622
  if (!apply) {
4253
- console.log(`DRIFT ${ref} merged: ${evidence} ${issue.title}`);
4623
+ console.log(
4624
+ `DRIFT ${ref} declared by: ${describeDeclarations(shas)} [${issue.roles.join(", ")}] ${issue.title}`
4625
+ );
4254
4626
  continue;
4255
4627
  }
4256
- const outcome = completeWorkItem(ref, contract);
4257
- // A sweep must not abort on the first item whose merges have not reached a
4258
- // deploy branch. It reports that item and carries on, which is what makes
4259
- // the backstop safe to run over a whole queue.
4260
- console.log(outcome.report);
4628
+ // A sweep must not abort on an item the applying arm refuses — whether it
4629
+ // refuses because the merge reached no deploy branch, or because the
4630
+ // tracker rejected the write. It reports that item and carries on, which
4631
+ // is what makes the backstop safe to run over a whole queue.
4632
+ try {
4633
+ console.log(completeWorkItem(ref, contract).report);
4634
+ } catch (error) {
4635
+ console.log(
4636
+ `work-item NOT completed: ${ref}\n${error instanceof Error ? error.message : String(error)}`
4637
+ );
4638
+ }
4261
4639
  }
4262
4640
  if (drifted === 0) {
4263
4641
  console.log(
4264
- `No drift: every item carrying "${claimed}" is genuinely in flight.`
4642
+ `No drift: every open item carrying ${examined} is genuinely in flight.\n` +
4643
+ `Examined ${subjects.size} item(s) across ${roles.length} lifecycle role(s); ` +
4644
+ `no role outside ${examined} was queried.`
4265
4645
  );
4266
4646
  return;
4267
4647
  }
4268
4648
  if (!apply) {
4269
4649
  console.log(
4270
- `\n${drifted} claimed item(s) already have a merged pull request. Re-run with --apply to complete them.`
4650
+ `\n${drifted} open item(s) carrying ${examined} are declared by a commit on a deploy branch. ` +
4651
+ `Re-run with --apply to complete them.`
4271
4652
  );
4272
4653
  }
4273
4654
  }
@@ -4327,7 +4708,8 @@ function validateCommit(args) {
4327
4708
  * that answer into silence.
4328
4709
  * @param {string} input The pre-push stdin stream.
4329
4710
  * @param {string} remote Remote being pushed to.
4330
- * @returns {{localRef: string|undefined, commits: string[], scope: string[]}[]}
4711
+ * @returns {{localRef: string|undefined, commits: string[], scope: string[],
4712
+ * examinedScope: string}[]}
4331
4713
  * Groups to check.
4332
4714
  */
4333
4715
  function pushGroups(input, remote) {
@@ -4335,7 +4717,17 @@ function pushGroups(input, remote) {
4335
4717
  const carrying = parsed.filter(group => group.commits.length > 0);
4336
4718
  return carrying.length > 0
4337
4719
  ? carrying
4338
- : [{ localRef: parsed[0]?.localRef, commits: [], scope: [] }];
4720
+ : [
4721
+ {
4722
+ commits: [],
4723
+ examinedScope: parsed[0]?.examinedScope ?? HEAD_FALLBACK_SCOPE,
4724
+ localRef: parsed[0]?.localRef,
4725
+ // Empty on purpose: `ancestryUnreachable` asks a question OF A RANGE,
4726
+ // and there is no range here. `examinedScope` above is the separate,
4727
+ // human-readable answer to "what did this verdict look at".
4728
+ scope: [],
4729
+ },
4730
+ ];
4339
4731
  }
4340
4732
 
4341
4733
  /**
@@ -4495,9 +4887,14 @@ function pushDeferralNote(result) {
4495
4887
  * the WIRING and the direct cases prove the WORDS.
4496
4888
  * @param {object} result Commit-side result.
4497
4889
  * @param {string} label Prefix naming the ref, empty for a single-ref push.
4890
+ * @param {string} [scope] The range or tree this verdict was computed over.
4498
4891
  * @returns {string} The whole report, replacing the success line.
4499
4892
  */
4500
- export function unresolvedPushReport(result, label) {
4893
+ export function unresolvedPushReport(
4894
+ result,
4895
+ label,
4896
+ scope = HEAD_FALLBACK_SCOPE
4897
+ ) {
4501
4898
  const contract = result.contract;
4502
4899
  const refs = result.refs ?? [];
4503
4900
  const declaration = refs.map(ref => `Work-Item: ${ref}`).join("\n ");
@@ -4506,7 +4903,7 @@ export function unresolvedPushReport(result, label) {
4506
4903
  ? ` UNRESOLVED gate 5 — each item needs a managed \`${MARKER}\` backlink comment pointing at the pull request. \`discharge-pr-gates\` posts it; nothing else has to remember to.`
4507
4904
  : ` n/a gate 5 — the tracker backlink is not required here: workItem.verify is "trailer".`;
4508
4905
  return [
4509
- `WORK_ITEM_TRACKING_INCOMPLETE ${label}${result.relevant} commit(s)${alreadyTraced(result)}: gates 1-3 proved here, 2 of 5 gates NOT CHECKED.`,
4906
+ `WORK_ITEM_TRACKING_INCOMPLETE ${label}${result.relevant} commit(s)${alreadyTraced(result)}${examined(scope)}: gates 1-3 proved here, 2 of 5 gates NOT CHECKED.`,
4510
4907
  ` This push is not a clean bill of health for ${refs.join(", ")}. ${pushDeferralNote(result)}`,
4511
4908
  ` UNRESOLVED gate 4 — the pull-request BODY must carry, on its own line${refs.length > 1 ? "s" : ""}:`,
4512
4909
  ` ${declaration}`,
@@ -4546,15 +4943,20 @@ export function pushSuccessLine(result, label) {
4546
4943
  * @param {{result?: object, error?: Error}} outcome Commit-side outcome.
4547
4944
  * @param {object|undefined} pr The pull request this ref is about, if any.
4548
4945
  * @param {string} label Prefix naming the ref, empty for a single-ref push.
4946
+ * @param {string} scope The range or tree this verdict was computed over.
4549
4947
  */
4550
- function reportPushGroup(outcome, pr, label) {
4948
+ function reportPushGroup(outcome, pr, label, scope) {
4551
4949
  if (pr) {
4552
4950
  // `true`: a push range is ALWAYS a subset of the pull request's — it drops
4553
4951
  // commits already on the remote branch and everything reachable from the
4554
4952
  // remote default branch. So a commit side with nothing in it means "no
4555
4953
  // subject in THIS push", never "no subject in the pull request".
4556
4954
  validatePrData(outcome, pr.url, pr.body, true);
4557
- console.log(pushSuccessLine(outcome.result, label));
4955
+ // The scope clause is appended HERE rather than inside `pushSuccessLine`
4956
+ // so that function keeps the two-argument shape #3886's inequality test
4957
+ // calls it with. What it returns is the verdict; what this adds is the
4958
+ // range the verdict was computed over.
4959
+ console.log(`${pushSuccessLine(outcome.result, label)}${examined(scope)}`);
4558
4960
  return;
4559
4961
  }
4560
4962
  // No pull request means gates 4 and 5 cannot be CHECKED here. They are
@@ -4571,16 +4973,22 @@ function reportPushGroup(outcome, pr, label) {
4571
4973
  // checklist teaches the reader to skip it.
4572
4974
  if ((outcome.result.refs ?? []).length === 0) {
4573
4975
  console.log(
4574
- `WORK_ITEM_TRACKING_OK ${label}${outcome.result.relevant} commit(s)${alreadyTraced(outcome.result)}; this range names no work item, so gates 4 and 5 have nothing to check here.`
4976
+ `WORK_ITEM_TRACKING_OK ${label}${outcome.result.relevant} commit(s)${alreadyTraced(outcome.result)}; this range names no work item, so gates 4 and 5 have nothing to check here.${examined(scope)}`
4575
4977
  );
4576
4978
  return;
4577
4979
  }
4578
- console.log(unresolvedPushReport(outcome.result, label));
4980
+ console.log(unresolvedPushReport(outcome.result, label, scope));
4579
4981
  }
4580
4982
 
4581
4983
  function validatePush(args) {
4582
- const remote = args[0] || "origin";
4583
- const input = readFileSync(0, "utf8");
4984
+ const remote = args[0] && !args[0].startsWith("-") ? args[0] : "origin";
4985
+ // `--refs <file>` / `LISA_PUSHED_REFS_FILE` names the captured pre-push
4986
+ // stream, exactly as `validate-push-destination` reads it. Without it this
4987
+ // command is reachable — through the gate runner — only after some earlier
4988
+ // reader has already spent git's stdin, and the empty-stream fallback below
4989
+ // then answers about the PUSHER'S `HEAD` rather than the pushed ref
4990
+ // (CodySwannGT/lisa#3874).
4991
+ const input = readPushRefs(option(args, "--refs", "LISA_PUSHED_REFS_FILE"));
4584
4992
  // The deploy chain is read from the remote default branch, the same ref this
4585
4993
  // path already trusts to bound the range. A local working tree could declare
4586
4994
  // anything, but so could a `--no-verify`; CI is the enforcing copy.
@@ -4599,7 +5007,8 @@ function validatePush(args) {
4599
5007
  reportPushGroup(
4600
5008
  outcome,
4601
5009
  group === target ? pr : undefined,
4602
- groups.length > 1 ? `${group.localRef ?? "(stdin)"}: ` : ""
5010
+ groups.length > 1 ? `${group.localRef ?? "(stdin)"}: ` : "",
5011
+ group.examinedScope
4603
5012
  );
4604
5013
  }
4605
5014
  }
@@ -4708,8 +5117,8 @@ export function postDischargeBacklinks(
4708
5117
  let changed = false;
4709
5118
  for (const ref of refs) {
4710
5119
  const outcome = post(ref, prUrl, contract);
4711
- changed ||= outcome !== "unchanged";
4712
- console.log(`work-item backlink ${outcome} on ${ref}: ${MARKER} ${prUrl}`);
5120
+ changed ||= outcome.change !== "unchanged";
5121
+ console.log(backlinkReport(outcome, ref, prUrl));
4713
5122
  }
4714
5123
  return changed;
4715
5124
  }