@joshuafolkken/kit 1.1455.0 → 1.1514.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 (374) hide show
  1. package/.claude/settings.json +6 -6
  2. package/.claude/skills/dependency-update/SKILL.md +26 -55
  3. package/.claude/skills/epic-commands/SKILL.md +83 -156
  4. package/.claude/skills/epic-commands/epic-bundle.md +51 -79
  5. package/.claude/skills/epic-commands/execution-waves.md +43 -81
  6. package/.claude/skills/verify-ui/SKILL.md +24 -50
  7. package/.claude/skills/workflow-commands/SKILL.md +7 -9
  8. package/.claude/skills/workflow-commands/background-commands.md +31 -46
  9. package/.claude/skills/workflow-commands/backlogrun-child.md +65 -85
  10. package/.claude/skills/workflow-commands/backlogrun-lanes.md +86 -125
  11. package/.claude/skills/workflow-commands/backlogrun-park.md +68 -118
  12. package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -28
  13. package/.claude/skills/workflow-commands/backlogrun-recovery.md +17 -26
  14. package/.claude/skills/workflow-commands/backlogrun-steps.md +35 -52
  15. package/.claude/skills/workflow-commands/backlogrun.md +31 -50
  16. package/.claude/skills/workflow-commands/chain-rule.md +31 -40
  17. package/.claude/skills/workflow-commands/delegation.md +12 -8
  18. package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
  19. package/.claude/skills/workflow-commands/followup.md +55 -77
  20. package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
  21. package/.claude/skills/workflow-commands/fullrun.md +4 -6
  22. package/.claude/skills/workflow-commands/halfrun.md +3 -7
  23. package/.claude/skills/workflow-commands/into-target.md +1 -2
  24. package/.claude/skills/workflow-commands/issue-comments.md +43 -60
  25. package/.claude/skills/workflow-commands/issue-scout.md +3 -2
  26. package/.claude/skills/workflow-commands/kickoff.md +65 -74
  27. package/.claude/skills/workflow-commands/latest-gate.md +2 -6
  28. package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
  29. package/.claude/skills/workflow-commands/observation-filing.md +59 -118
  30. package/.claude/skills/workflow-commands/observation-ledger.md +52 -112
  31. package/.claude/skills/workflow-commands/pre-gate-cut.md +88 -142
  32. package/.claude/skills/workflow-commands/prerequisite.md +11 -17
  33. package/.claude/skills/workflow-commands/progress-watcher.md +23 -33
  34. package/.claude/skills/workflow-commands/prrun.md +3 -5
  35. package/.claude/skills/workflow-commands/retrospective.md +35 -54
  36. package/.claude/skills/workflow-commands/split-assessment.md +32 -58
  37. package/.claude/skills/workflow-commands/target-repository.md +1 -2
  38. package/.claude/skills/workflow-commands/working-tree-hold.md +54 -73
  39. package/.codex/hooks.json +6 -6
  40. package/.github/actions/setup-node/action.yml +1 -1
  41. package/.github/workflows/ci.yml +1 -1
  42. package/.github/workflows/publish.yml +2 -2
  43. package/CLAUDE.md +7 -6
  44. package/cspell/index.yaml +7 -0
  45. package/dist/CLAUDE.md +7 -6
  46. package/dist/commands/document-section-cli.js +67 -34
  47. package/dist/hooks/batch-guard.js +6 -6
  48. package/dist/hooks/chunk-4KKD2UTK.js +66 -0
  49. package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-6TS2JD6D.js} +14 -3
  50. package/dist/hooks/chunk-7DSBJVFH.js +50 -0
  51. package/dist/hooks/{chunk-5H4SSDSD.js → chunk-A65Y4YOR.js} +3 -3
  52. package/dist/hooks/{chunk-3C4XRYG6.js → chunk-APUO6U4J.js} +102 -19
  53. package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
  54. package/dist/hooks/chunk-GP4IEC4O.js +20 -0
  55. package/dist/hooks/chunk-JP45SOCJ.js +56 -0
  56. package/dist/hooks/{chunk-OLMO72ON.js → chunk-LAOV7NLM.js} +1 -1
  57. package/dist/hooks/chunk-M4HGZV7U.js +83 -0
  58. package/dist/hooks/{chunk-TBJFTSUY.js → chunk-O6GDRNJB.js} +9 -7
  59. package/dist/hooks/{chunk-HFGWYGUV.js → chunk-PF2NZL2A.js} +5 -5
  60. package/dist/hooks/{chunk-WNLVGT7E.js → chunk-PTICX4OL.js} +4 -5
  61. package/dist/hooks/{chunk-HHI6FO3R.js → chunk-RT27QRJW.js} +763 -1515
  62. package/dist/hooks/{chunk-7WQBPLUN.js → chunk-SYOTDYCI.js} +1149 -1095
  63. package/dist/hooks/{chunk-LIAFCXL7.js → chunk-TNUMY6NL.js} +1519 -1332
  64. package/dist/hooks/chunk-UPUZF3AN.js +279 -0
  65. package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-WK6SMBIC.js} +1932 -784
  66. package/dist/hooks/codex-hook-adapter.js +17 -14
  67. package/dist/hooks/duplicate-read-guard.js +7 -7
  68. package/dist/hooks/format-edited.js +5 -4
  69. package/dist/hooks/inputs.json +302 -247
  70. package/dist/hooks/investigation-guard.js +8 -11
  71. package/dist/hooks/pretool-guard.js +16 -13
  72. package/dist/hooks/session-lang.js +372 -26
  73. package/dist/hooks/stop-guard.js +2593 -189
  74. package/dist/josh.js +2128 -282
  75. package/dist/managed-marker/index.js +1 -2
  76. package/dist/version/index.js +15 -12
  77. package/eslint/base.js +2 -1
  78. package/eslint/rules/unicorn.js +16 -0
  79. package/lefthook/base.yml +5 -3
  80. package/package.json +14 -12
  81. package/prompts/coding-standards.md +12 -22
  82. package/prompts/collaboration-workflow/file-edits.md +25 -45
  83. package/prompts/collaboration-workflow/gh-rest.md +19 -36
  84. package/prompts/collaboration-workflow/glossary.md +5 -7
  85. package/prompts/collaboration-workflow/issue-citation.md +6 -14
  86. package/prompts/collaboration-workflow/issue-template.md +40 -32
  87. package/prompts/collaboration-workflow/lane-instructions.md +35 -0
  88. package/prompts/collaboration-workflow/operating-rules.md +33 -46
  89. package/prompts/collaboration-workflow/output-bounds.md +15 -15
  90. package/prompts/collaboration-workflow/overview.md +14 -19
  91. package/prompts/collaboration-workflow/plan-comment.md +13 -13
  92. package/prompts/collaboration-workflow/principles.md +57 -54
  93. package/prompts/collaboration-workflow/proposal-request.md +1 -9
  94. package/prompts/collaboration-workflow/report-format.md +41 -49
  95. package/prompts/collaboration-workflow/residency.md +15 -11
  96. package/prompts/collaboration-workflow/rule-delivery.md +7 -15
  97. package/prompts/collaboration-workflow/shell-body.md +10 -20
  98. package/prompts/collaboration-workflow/turn-batching.md +8 -14
  99. package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
  100. package/prompts/collaboration-workflow/wip-cap.md +26 -32
  101. package/prompts/collaboration-workflow.md +25 -25
  102. package/prompts/refactoring.md +33 -43
  103. package/prompts/review-rubric.md +84 -129
  104. package/prompts/review.md +110 -141
  105. package/prompts/sonar-hotspot-handling.md +22 -50
  106. package/prompts/testing-guide.md +48 -99
  107. package/scripts/adopt/adopt-toolkits.ts +2 -4
  108. package/scripts/agent/agent-argv.ts +20 -4
  109. package/scripts/agent/agent-exit-record.ts +23 -1
  110. package/scripts/agent/agent-role-profile.ts +101 -45
  111. package/scripts/agent/agent-safe-text.ts +60 -0
  112. package/scripts/agent/claude-agent-argv.ts +14 -0
  113. package/scripts/agent/codex-agent-argv.ts +9 -2
  114. package/scripts/agent/lane-instructions.ts +144 -0
  115. package/scripts/backlog/backlog-budget-cli.ts +1 -2
  116. package/scripts/backlog/backlog-budget.ts +4 -4
  117. package/scripts/backlog/backlog-drive-carried.ts +26 -0
  118. package/scripts/backlog/backlog-drive-cli.ts +10 -5
  119. package/scripts/backlog/backlog-drive-named.ts +3 -5
  120. package/scripts/backlog/backlog-drive-restore.ts +28 -2
  121. package/scripts/backlog/backlog-drive-seats.ts +27 -0
  122. package/scripts/backlog/backlog-drive.ts +11 -6
  123. package/scripts/backlog/backlog-occupied.ts +56 -0
  124. package/scripts/backlog/backlog-ready.ts +59 -15
  125. package/scripts/behavior/behavior-rules.ts +3 -2
  126. package/scripts/cost-runtime/codex-usage.ts +4 -2
  127. package/scripts/cost-runtime/cost-cli.ts +1 -2
  128. package/scripts/delegation/duplicate-read-guard.ts +2 -2
  129. package/scripts/delegation/investigation-guard.ts +15 -65
  130. package/scripts/delegation/investigation-reads.ts +1 -2
  131. package/scripts/discovery/repo-discovery.ts +4 -2
  132. package/scripts/discovery/repo-origin.ts +2 -4
  133. package/scripts/doctor/doctor-logic.ts +6 -3
  134. package/scripts/document/document-byte-budget.ts +28 -27
  135. package/scripts/document/document-byte-check.ts +1 -2
  136. package/scripts/document/document-section-cli.ts +17 -5
  137. package/scripts/document/entry-read-budget.ts +3 -3
  138. package/scripts/document/read-set-cli.ts +4 -1
  139. package/scripts/epic/epic-audit-orphans.ts +5 -8
  140. package/scripts/epic/epic-audit-rationale.ts +8 -2
  141. package/scripts/epic/epic-audit.ts +52 -1
  142. package/scripts/epic/epic-bundle-cli.ts +1 -2
  143. package/scripts/epic/epic-bundle-evidence.ts +49 -26
  144. package/scripts/epic/epic-bundle-referenced.ts +14 -3
  145. package/scripts/epic/epic-bundle.ts +17 -9
  146. package/scripts/epic/epic-chains.ts +6 -2
  147. package/scripts/epic/epic-close-comment.ts +1 -2
  148. package/scripts/followup/git-followup-workflow.ts +19 -1
  149. package/scripts/gate/check-inputs.ts +129 -0
  150. package/scripts/gate/check-reuse.ts +162 -0
  151. package/scripts/gate/core-budget.ts +93 -44
  152. package/scripts/gate/core-wait.ts +130 -0
  153. package/scripts/gate/free-cores.ts +49 -0
  154. package/scripts/gate/gate-ledger.ts +17 -3
  155. package/scripts/gate/gate-plan.ts +23 -1
  156. package/scripts/gate/gate-report.ts +15 -2
  157. package/scripts/gate/gate-run-steps.ts +150 -0
  158. package/scripts/gate/held-mark.ts +55 -0
  159. package/scripts/gate/machine-capacity.ts +70 -11
  160. package/scripts/gate/pre-commit-type-check.ts +14 -1
  161. package/scripts/gate/verification-gate.ts +65 -127
  162. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  163. package/scripts/gh/git-gh-issue-write.ts +24 -2
  164. package/scripts/gh/git-pr.ts +19 -3
  165. package/scripts/git/git-common-directory.ts +3 -3
  166. package/scripts/git/git-diff-reads.ts +16 -9
  167. package/scripts/git/git-execa-error.ts +1 -2
  168. package/scripts/git/main-merge-regenerate.ts +78 -0
  169. package/scripts/git/main-merge.ts +14 -5
  170. package/scripts/hooks/batch-guard.ts +5 -8
  171. package/scripts/hooks/format-edited-file.ts +10 -4
  172. package/scripts/hooks/pre-commit-lint.ts +91 -0
  173. package/scripts/hooks/pre-push-unit.ts +15 -4
  174. package/scripts/hooks/pretool-guard.ts +48 -79
  175. package/scripts/hooks/reserved-run.ts +1 -0
  176. package/scripts/hooks/run-hook.sh +10 -12
  177. package/scripts/hooks/step-zero-notice.ts +3 -2
  178. package/scripts/hooks/stop-actions.ts +204 -0
  179. package/scripts/hooks/stop-guard.ts +62 -21
  180. package/scripts/init/hook-command-rewrite.ts +6 -2
  181. package/scripts/init/hook-launch.ts +12 -7
  182. package/scripts/init/init-logic-deploy-vps.ts +4 -2
  183. package/scripts/issue/behavior-change-lint.ts +12 -6
  184. package/scripts/issue/expected-effect-lint.ts +59 -0
  185. package/scripts/issue/issue-cite-cli.ts +35 -15
  186. package/scripts/issue/issue-classification.ts +1 -1
  187. package/scripts/issue/issue-lint-cli.ts +5 -3
  188. package/scripts/issue/issue-scout-cli.ts +22 -16
  189. package/scripts/issue/markdown-section.ts +19 -5
  190. package/scripts/josh/command-usage.ts +110 -0
  191. package/scripts/josh/file-map-stamp.ts +1 -3
  192. package/scripts/josh/hook-decision.ts +17 -2
  193. package/scripts/josh/josh-command-map.ts +2 -0
  194. package/scripts/josh/josh-command-types.ts +55 -9
  195. package/scripts/josh/josh-commands-ai.ts +13 -0
  196. package/scripts/josh/josh-commands-development.ts +1 -1
  197. package/scripts/josh/josh-commands-hooks.ts +6 -0
  198. package/scripts/josh/josh-commands-lane.ts +10 -1
  199. package/scripts/josh/josh-commands-workflow.ts +5 -0
  200. package/scripts/josh/josh-logic.ts +6 -15
  201. package/scripts/josh/josh-usage-automation.ts +121 -0
  202. package/scripts/josh/josh-usage-backlog.ts +180 -0
  203. package/scripts/josh/josh-usage-developer.ts +230 -0
  204. package/scripts/josh/josh-usage-hooks.ts +38 -0
  205. package/scripts/josh/josh-usage-issue.ts +205 -0
  206. package/scripts/josh/josh-usage-review.ts +101 -0
  207. package/scripts/josh/josh-usage-run.ts +252 -0
  208. package/scripts/josh/josh-usage-session.ts +238 -0
  209. package/scripts/josh/josh-usage-workflow.ts +212 -0
  210. package/scripts/josh/josh.ts +14 -5
  211. package/scripts/josh/package-directory.ts +20 -0
  212. package/scripts/lane/lane-await.ts +3 -2
  213. package/scripts/lane/lane-capacity.ts +8 -2
  214. package/scripts/lane/lane-check-stats.ts +56 -0
  215. package/scripts/lane/lane-child-invocation.ts +49 -5
  216. package/scripts/lane/lane-dispatch.ts +3 -3
  217. package/scripts/lane/lane-guard-policy.ts +5 -5
  218. package/scripts/lane/lane-handoff.ts +8 -1
  219. package/scripts/lane/lane-install.ts +12 -4
  220. package/scripts/lane/lane-ledger.ts +52 -1
  221. package/scripts/lane/lane-limit-cli.ts +8 -9
  222. package/scripts/lane/lane-load.ts +9 -2
  223. package/scripts/lane/lane-measure-cli.ts +4 -0
  224. package/scripts/lane/lane-parked.ts +18 -0
  225. package/scripts/lane/lane-preserve.ts +29 -1
  226. package/scripts/lane/lane-process-load.ts +125 -0
  227. package/scripts/lane/lane-process-stats.ts +93 -0
  228. package/scripts/lane/lane-registry.ts +17 -10
  229. package/scripts/lane/lane-restart.ts +5 -26
  230. package/scripts/lane/lane-session-action.ts +146 -0
  231. package/scripts/lane/lane-session-broker.ts +68 -125
  232. package/scripts/lane/lane-spent.ts +14 -3
  233. package/scripts/lane/lane-stats.ts +1 -0
  234. package/scripts/lane/lane-withdraw-cli.ts +71 -0
  235. package/scripts/lane/lane-withdraw.ts +182 -0
  236. package/scripts/lint/format.ts +4 -4
  237. package/scripts/lint/lint-parallel.ts +10 -3
  238. package/scripts/lint/lint-related-scope.ts +4 -1
  239. package/scripts/managed-marker/managed-marker-logic.ts +1 -2
  240. package/scripts/notify/telegram-test.ts +10 -5
  241. package/scripts/overrides/lockfile-overrides.ts +1 -3
  242. package/scripts/propagate/propagate.ts +1 -2
  243. package/scripts/release/release-progress.ts +3 -2
  244. package/scripts/report/report-format-reference.ts +2 -0
  245. package/scripts/report/report-lint.ts +7 -4
  246. package/scripts/review/acceptance-map-check.ts +61 -0
  247. package/scripts/review/acceptance-map.ts +205 -0
  248. package/scripts/review/review-brief-cli.ts +1 -1
  249. package/scripts/review/review-record-cli.ts +6 -2
  250. package/scripts/rules/agent-model.ts +59 -0
  251. package/scripts/rules/delivered-rules.ts +11 -2
  252. package/scripts/rules/destructive-command.ts +6 -9
  253. package/scripts/rules/direct-filing.ts +3 -7
  254. package/scripts/rules/early-heartbeat.ts +7 -18
  255. package/scripts/rules/file-body.ts +6 -13
  256. package/scripts/rules/filing-cap.ts +4 -6
  257. package/scripts/rules/firing-point.ts +6 -5
  258. package/scripts/rules/git-force.ts +5 -9
  259. package/scripts/rules/guard-remedy.ts +117 -0
  260. package/scripts/rules/implementation-cut.ts +37 -35
  261. package/scripts/rules/index-guard.ts +4 -7
  262. package/scripts/rules/issue-comments.ts +67 -16
  263. package/scripts/rules/josh-git-bare.ts +3 -6
  264. package/scripts/rules/lane-background.ts +10 -21
  265. package/scripts/rules/lane-carry-conflict.ts +7 -17
  266. package/scripts/rules/lane-interactive-ask.ts +6 -11
  267. package/scripts/rules/lane-park.ts +5 -13
  268. package/scripts/rules/lane-split-park.ts +2 -7
  269. package/scripts/rules/lane-switch-main.ts +9 -12
  270. package/scripts/rules/last-prompt.ts +50 -5
  271. package/scripts/rules/oracle-consulted.ts +4 -5
  272. package/scripts/rules/oracle-list-cli.ts +7 -0
  273. package/scripts/rules/piped-verification.ts +84 -32
  274. package/scripts/rules/poll-loop.ts +7 -17
  275. package/scripts/rules/pre-gate-cut.ts +7 -21
  276. package/scripts/rules/prior-comment-read.ts +7 -5
  277. package/scripts/rules/protected-files.ts +5 -9
  278. package/scripts/rules/raw-field-body.ts +3 -7
  279. package/scripts/rules/rule-body-guard.ts +4 -11
  280. package/scripts/rules/rule-list.ts +4 -1
  281. package/scripts/rules/rule-registry-rows.ts +10 -0
  282. package/scripts/rules/rule-registry.ts +7 -4
  283. package/scripts/rules/rule-rewrite.ts +114 -0
  284. package/scripts/rules/rule-value.ts +7 -1
  285. package/scripts/rules/run-tail-rule.ts +6 -22
  286. package/scripts/rules/shell-body-trigger.ts +4 -6
  287. package/scripts/rules/shell-segments.ts +22 -0
  288. package/scripts/rules/stop-decision.ts +77 -0
  289. package/scripts/rules/stop-rules.ts +164 -77
  290. package/scripts/rules/tail-commands.ts +15 -1
  291. package/scripts/rules/test-declared-commit.ts +4 -10
  292. package/scripts/rules/third-party-write.ts +5 -10
  293. package/scripts/rules/worktree-guard.ts +5 -12
  294. package/scripts/run/board/run-board-cli.ts +2 -0
  295. package/scripts/run/board/run-board-closed.ts +8 -2
  296. package/scripts/run/board/run-board-github.ts +19 -3
  297. package/scripts/run/board/run-board-header.ts +4 -1
  298. package/scripts/run/board/run-board-idle.ts +31 -0
  299. package/scripts/run/board/run-board-labels.ts +2 -0
  300. package/scripts/run/board/run-board-layout.ts +1 -1
  301. package/scripts/run/board/run-board-read.ts +25 -12
  302. package/scripts/run/board/run-board-reason.ts +17 -4
  303. package/scripts/run/board/run-board-render-legend.ts +1 -0
  304. package/scripts/run/board/run-board-render-slot.ts +1 -1
  305. package/scripts/run/board/run-board-render-waits.ts +41 -0
  306. package/scripts/run/board/run-board-render.ts +6 -1
  307. package/scripts/run/board/run-board-row-yield.ts +1 -0
  308. package/scripts/run/board/run-board-state.ts +9 -0
  309. package/scripts/run/board/run-board-status.ts +89 -23
  310. package/scripts/run/board/run-board-thanks.ts +7 -2
  311. package/scripts/run/board/run-board-tick.ts +37 -8
  312. package/scripts/run/carry/run-carry-args.ts +1 -3
  313. package/scripts/run/carry/run-carry-conversation.ts +7 -2
  314. package/scripts/run/cut/run-cut-cli.ts +18 -22
  315. package/scripts/run/cut/run-cut-decline.ts +72 -0
  316. package/scripts/run/cut/run-cut-ready.ts +74 -0
  317. package/scripts/run/cut/run-cut-report.ts +29 -0
  318. package/scripts/run/cut/run-cut.ts +10 -1
  319. package/scripts/run/detached-launch.ts +23 -4
  320. package/scripts/run/entry/run-entry-cli.ts +8 -1
  321. package/scripts/run/event/run-event-render.ts +1 -0
  322. package/scripts/run/event/run-event-scope.ts +19 -1
  323. package/scripts/run/event/run-event-stream-emit.ts +13 -6
  324. package/scripts/run/event/run-event-stream.ts +4 -0
  325. package/scripts/run/hold/run-hold-cli.ts +14 -3
  326. package/scripts/run/hold/run-hold-session.ts +46 -0
  327. package/scripts/run/hold/run-hold.ts +13 -4
  328. package/scripts/run/launch-priority.ts +32 -0
  329. package/scripts/run/merge/run-merge-cli.ts +16 -42
  330. package/scripts/run/merge/run-merge-ship.ts +59 -1
  331. package/scripts/run/merge/run-merge-signals.ts +99 -0
  332. package/scripts/run/merge/run-merge-steps.ts +11 -1
  333. package/scripts/run/merge/run-merge.ts +22 -9
  334. package/scripts/run/run-ending.ts +31 -3
  335. package/scripts/run/run-liveness.ts +1 -1
  336. package/scripts/run/run-parent-cut-hook.ts +5 -10
  337. package/scripts/run/run-poll-hook.ts +157 -0
  338. package/scripts/run/run-preflight.ts +1 -2
  339. package/scripts/run/run-step-cli.ts +22 -15
  340. package/scripts/run/run-step-section.ts +85 -0
  341. package/scripts/run/run-stop-notify.ts +1 -2
  342. package/scripts/run/run-watcher-guard.ts +6 -3
  343. package/scripts/run/ship/run-ship-cli.ts +5 -12
  344. package/scripts/run/ship/run-ship-detach.ts +9 -0
  345. package/scripts/run/ship/run-ship-next.ts +10 -2
  346. package/scripts/run/ship/run-ship-overlap.ts +195 -0
  347. package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
  348. package/scripts/run/ship/run-ship-preflight.ts +28 -2
  349. package/scripts/run/ship/run-ship-return.ts +31 -7
  350. package/scripts/run/ship/run-ship-review-steps.ts +25 -14
  351. package/scripts/run/ship/run-ship-review.ts +15 -2
  352. package/scripts/run/ship/run-ship-steps.ts +1 -1
  353. package/scripts/run/ship/run-ship-stop-context.ts +206 -0
  354. package/scripts/run/ship/run-ship-sync.ts +14 -6
  355. package/scripts/run/tidy/run-tidy-cli.ts +3 -3
  356. package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
  357. package/scripts/safe-chain/project-config.ts +7 -2
  358. package/scripts/sync/sync-hook-safety.ts +8 -2
  359. package/scripts/test/test-e2e-guard.ts +30 -5
  360. package/scripts/test/test-unit-guard.ts +16 -1
  361. package/scripts/test/unit-worker-share.ts +26 -3
  362. package/scripts/time-runtime/time-batch-guard.ts +3 -3
  363. package/scripts/time-runtime/time-hook-transcript.ts +3 -2
  364. package/scripts/time-runtime/time-shell.ts +39 -4
  365. package/scripts/time-runtime/time-spans.ts +1 -0
  366. package/scripts/time-runtime/time-tool-call.ts +7 -0
  367. package/scripts/version/effective-upstream.ts +3 -4
  368. package/scripts/version/kit-version-config.ts +8 -2
  369. package/scripts/version/latest-corepack.ts +1 -2
  370. package/scripts/version/release-hold.ts +11 -4
  371. package/scripts/version/version-commands.ts +12 -4
  372. package/templates/gitignore +6 -2
  373. package/dist/hooks/chunk-D5QBTOCZ.js +0 -68
  374. package/dist/hooks/chunk-UT7YC3N2.js +0 -56
@@ -180,6 +180,55 @@ function acceptance_section(body: string | undefined): string {
180
180
  return markdown_section.section_lines_matching(body, is_acceptance_heading).join('\n')
181
181
  }
182
182
 
183
+ // Where a body reference that counts as a relation was written. Only these two places state what the
184
+ // issue delivers or where it belongs; a number in the background, symptom or origin section is the
185
+ // example that prompted the filing, and reading it as a relation bundled issues whose work never
186
+ // overlaps.
187
+ type SignalSection = 'acceptance criteria' | 'parent line'
188
+
189
+ interface SignalReference {
190
+ number: number
191
+ section: SignalSection
192
+ }
193
+
194
+ // What marks a line as naming the issue's parent, in both spellings for the same reason as
195
+ // `ACCEPTANCE_HEADINGS`. `epic:audit`'s orphan search matches it anywhere in the line, then asks
196
+ // whether the same line names the epic.
197
+ const PARENT_MARKERS: ReadonlyArray<string> = ['親:', '親:', 'Parent:', 'parent:']
198
+
199
+ function has_parent_marker(line: string): boolean {
200
+ return PARENT_MARKERS.some((marker) => line.includes(marker))
201
+ }
202
+
203
+ // The reference written directly after a parent marker, bold markup allowed (`- **Parent:** #N`).
204
+ // Only that reference is the parent: a background sentence that mentions `parent:` and names an
205
+ // issue elsewhere on the line is not a parent line.
206
+ const PARENT_REFERENCE = /(?:親|Parent|parent)\**[::]\**\s*(?:[\w.-]+\/[\w.-]+)?#\d+/gu
207
+
208
+ function parent_references(body: string): string {
209
+ return markdown_section
210
+ .unfenced_lines(body)
211
+ .flatMap((line) => [...line.matchAll(PARENT_REFERENCE)].map((match) => match[0]))
212
+ .join('\n')
213
+ }
214
+
215
+ function references_in(text: string, repo: string, section: SignalSection): Array<SignalReference> {
216
+ return parse_references(text, repo).map((number) => ({ number, section }))
217
+ }
218
+
219
+ // The same-repository references a body makes in the places that count as a relation, each with the
220
+ // section it was read from. A number both places name is reported once, at the first.
221
+ function signal_references(body: string, repo = ''): Array<SignalReference> {
222
+ const read = [
223
+ ...references_in(acceptance_section(body), repo, 'acceptance criteria'),
224
+ ...references_in(parent_references(body), repo, 'parent line'),
225
+ ]
226
+
227
+ return read.filter((reference, index) => {
228
+ return read.findIndex((other) => other.number === reference.number) === index
229
+ })
230
+ }
231
+
183
232
  // Everything `node` depends on, directly or through a chain. Walked iteratively so a graph that
184
233
  // still contains a cycle terminates — `epic:next` rejects one first, but an auditor must not hang on
185
234
  // the input it exists to examine.
@@ -223,8 +272,10 @@ const epic_audit_logic = {
223
272
  parse_issue_references,
224
273
  parse_references,
225
274
  acceptance_section,
275
+ signal_references,
276
+ has_parent_marker,
226
277
  depends_on,
227
278
  }
228
279
 
229
- export type { AuditFinding, FindingLevel, ReferenceState }
280
+ export type { AuditFinding, FindingLevel, ReferenceState, SignalReference, SignalSection }
230
281
  export { epic_audit_logic }
@@ -323,12 +323,11 @@ async function widen_with_referenced(
323
323
  backlog: FetchedBacklog,
324
324
  ): Promise<FetchedBacklog> {
325
325
  const { context } = backlog
326
- if (context === undefined) return backlog
327
326
  // An issue an epic already tracks has nothing to bundle — `decide_bundle` answers from that alone
328
327
  // and never looks at the candidates. Widening first spends a request per reference on a verdict
329
328
  // that ignores them, and prints "⚠ Could not read #891." above "Already in an epic", which reads
330
329
  // as a gap in an answer the read was never part of.
331
- if (subject.epic !== undefined) return backlog
330
+ if (context === undefined || subject.epic !== undefined) return backlog
332
331
  const known = new Set(backlog.issues.map((issue) => issue.number))
333
332
  const found = await epic_bundle_referenced.referenced_candidates(subject, known, context)
334
333
  if (found === undefined) return backlog
@@ -1,4 +1,5 @@
1
1
  import { issue_cite } from '#scripts/issue/issue-cite'
2
+ import type { SignalSection } from './epic-audit'
2
3
  import { epic_bundle, type BacklogIssue } from './epic-bundle'
3
4
 
4
5
  // The facts the bundle decision was already made from, printed beside the order it prints.
@@ -20,10 +21,26 @@ import { epic_bundle, type BacklogIssue } from './epic-bundle'
20
21
  type EvidenceKind = 'dependency' | 'reference'
21
22
 
22
23
  // One fact, and the order it points to. `first` is the issue the fact puts ahead of `second`.
24
+ // `section` is where a reference was read, so the reader sees it was the acceptance criteria or the
25
+ // parent line that counted, not a mention elsewhere in the body.
23
26
  interface OrderingEvidence {
24
27
  kind: EvidenceKind
25
28
  first: number
26
29
  second: number
30
+ section?: SignalSection
31
+ }
32
+
33
+ // What one directed test found: nothing, or the fact with whatever detail it carries.
34
+ type EvidenceFound = Pick<OrderingEvidence, 'section'> | undefined
35
+
36
+ function blocker_found(from: BacklogIssue, to: BacklogIssue): EvidenceFound {
37
+ return epic_bundle.names_as_blocker(from, to) ? {} : undefined
38
+ }
39
+
40
+ function reference_found(from: BacklogIssue, to: BacklogIssue): EvidenceFound {
41
+ const section = epic_bundle.reference_section(from, to)
42
+
43
+ return section === undefined ? undefined : { section }
27
44
  }
28
45
 
29
46
  // The two readings, each written as a directed test of one issue against another. Both are
@@ -34,14 +51,28 @@ interface OrderingEvidence {
34
51
  // `blocked-by` says so outright: the blocker has to land before the issue that declares it. A
35
52
  // reference says it by having been written — the issue that names another was drafted with that
36
53
  // other one already in existence, so the named one is the earlier of the two.
37
- const EVIDENCE_TESTS: ReadonlyArray<{
54
+ interface EvidenceTest {
38
55
  kind: EvidenceKind
39
- holds: (from: BacklogIssue, to: BacklogIssue) => boolean
40
- }> = [
41
- { kind: 'dependency', holds: (from, to) => epic_bundle.names_as_blocker(from, to) },
42
- { kind: 'reference', holds: (from, to) => epic_bundle.names_in_body(from, to) },
56
+ find: (from: BacklogIssue, to: BacklogIssue) => EvidenceFound
57
+ }
58
+
59
+ const EVIDENCE_TESTS: ReadonlyArray<EvidenceTest> = [
60
+ { kind: 'dependency', find: blocker_found },
61
+ { kind: 'reference', find: reference_found },
43
62
  ]
44
63
 
64
+ // One test in one direction: the fact it found, or none.
65
+ function directed_evidence(
66
+ test: EvidenceTest,
67
+ from: BacklogIssue,
68
+ to: BacklogIssue,
69
+ ): Array<OrderingEvidence> {
70
+ const found = test.find(from, to)
71
+ if (found === undefined) return []
72
+
73
+ return [{ kind: test.kind, first: to.number, second: from.number, ...found }]
74
+ }
75
+
45
76
  // Both directions of one pair, because both are facts. Two issues that name each other produce two
46
77
  // lines pointing opposite ways, which is the honest report: the reader is the one deciding.
47
78
  function pair_evidence(subject: BacklogIssue, other: BacklogIssue): Array<OrderingEvidence> {
@@ -50,10 +81,8 @@ function pair_evidence(subject: BacklogIssue, other: BacklogIssue): Array<Orderi
50
81
  { from: other, to: subject },
51
82
  ]
52
83
 
53
- return EVIDENCE_TESTS.flatMap(({ kind, holds }) =>
54
- directions
55
- .filter((pair) => holds(pair.from, pair.to))
56
- .map((pair) => ({ kind, first: pair.to.number, second: pair.from.number })),
84
+ return EVIDENCE_TESTS.flatMap((test) =>
85
+ directions.flatMap((pair) => directed_evidence(test, pair.from, pair.to)),
57
86
  )
58
87
  }
59
88
 
@@ -85,8 +114,8 @@ function ordering_evidence(
85
114
  return member_pairs(members).flatMap((pair) => pair_evidence(pair.left, pair.right))
86
115
  }
87
116
 
88
- // How each kind reads. `where` says the fact came from prose rather than from a recorded relation,
89
- // and the two `order_*` halves say how strongly the fact settles the order.
117
+ // How each kind reads. The two `order_*` halves say how strongly the fact settles the order; where a
118
+ // reference was read comes from the fact itself (`section_phrase`).
90
119
  //
91
120
  // **A recorded dependency states the order; a reference only points at it.** A `blocked-by` says
92
121
  // outright that the blocker lands first. A reference says it by having been written — which holds
@@ -95,20 +124,14 @@ function ordering_evidence(
95
124
  // the reference line reports the direction it points to and asserts nothing further; deciding is
96
125
  // still the reader's.
97
126
  const EVIDENCE_WORDING: Readonly<
98
- Record<EvidenceKind, { verb: string; where: string; order_prefix: string; order_suffix: string }>
127
+ Record<EvidenceKind, { verb: string; order_prefix: string; order_suffix: string }>
99
128
  > = {
100
- dependency: {
101
- verb: 'is recorded as blocked by',
102
- where: '',
103
- order_prefix: '',
104
- order_suffix: ' comes first',
105
- },
106
- reference: {
107
- verb: 'names',
108
- where: ' in its body',
109
- order_prefix: 'that points to ',
110
- order_suffix: ' first',
111
- },
129
+ dependency: { verb: 'is recorded as blocked by', order_prefix: '', order_suffix: ' comes first' },
130
+ reference: { verb: 'names', order_prefix: 'that points to ', order_suffix: ' first' },
131
+ }
132
+
133
+ function section_phrase(item: OrderingEvidence): string {
134
+ return item.section === undefined ? '' : ` in its ${item.section}`
112
135
  }
113
136
 
114
137
  const EVIDENCE_HEADING = ' Evidence:'
@@ -117,12 +140,12 @@ const EVIDENCE_INDENT_WIDTH = 4
117
140
  const EVIDENCE_INDENT = ' '.repeat(EVIDENCE_INDENT_WIDTH)
118
141
 
119
142
  function format_one(item: OrderingEvidence): string {
120
- const { verb, where, order_prefix, order_suffix } = EVIDENCE_WORDING[item.kind]
143
+ const { verb, order_prefix, order_suffix } = EVIDENCE_WORDING[item.kind]
121
144
  const first = issue_cite.plain(item.first)
122
145
  const second = issue_cite.plain(item.second)
123
146
  const order = `${order_prefix}${first}${order_suffix}`
124
147
 
125
- return `${EVIDENCE_INDENT}${second} ${verb} ${first}${where} — ${order}`
148
+ return `${EVIDENCE_INDENT}${second} ${verb} ${first}${section_phrase(item)} — ${order}`
126
149
  }
127
150
 
128
151
  // The block that goes under `Order:`. Empty when neither reading found anything, which leaves the
@@ -44,10 +44,21 @@ interface ReferencedLookups {
44
44
  dropped: Array<number>
45
45
  }
46
46
 
47
+ // Every number the body names, the ones a bundle signal counts first: the cap below must not leave a
48
+ // signal unread because twenty background mentions came ahead of it in the body.
49
+ function signal_first(subject: BacklogIssue): Array<number> {
50
+ const signals = epic_audit_logic
51
+ .signal_references(subject.body, subject.repo)
52
+ .map((reference) => reference.number)
53
+ const all = epic_audit_logic.parse_references(subject.body, subject.repo)
54
+
55
+ return [...new Set([...signals, ...all])]
56
+ }
57
+
47
58
  function referenced_lookups(subject: BacklogIssue, known: ReadonlySet<number>): ReferencedLookups {
48
- const candidates = epic_audit_logic
49
- .parse_references(subject.body, subject.repo)
50
- .filter((number) => number !== subject.number && !known.has(number))
59
+ const candidates = signal_first(subject).filter(
60
+ (number) => number !== subject.number && !known.has(number),
61
+ )
51
62
 
52
63
  return {
53
64
  numbers: candidates.slice(0, REFERENCED_LOOKUP_LIMIT),
@@ -1,6 +1,6 @@
1
1
  import { issue_cite } from '#scripts/issue/issue-cite'
2
2
  import { has_label_name, RELEASE_LABEL } from '#scripts/issue/issue-labels'
3
- import { epic_audit_logic } from './epic-audit'
3
+ import { epic_audit_logic, type SignalSection } from './epic-audit'
4
4
  import { epic_graph } from './epic-graph'
5
5
  import type { DependencyLink } from './epic-parse'
6
6
  import type { IssueReference } from './epic-reference'
@@ -11,9 +11,9 @@ import type { IssueReference } from './epic-reference'
11
11
  // not reach the other way in: two issues filed days apart that turn out to be the front and back of
12
12
  // one job are executed separately, in whatever order, with the reasoning recorded nowhere.
13
13
  //
14
- // The candidate search reads issue references out of prose — the same reading `epic:audit` does
15
- // inside one epic. That analysis is imported rather than written a second time; the only
16
- // difference is what it is pointed at.
14
+ // The candidate search reads issue references out of the acceptance criteria and the `parent: #N`
15
+ // line — the same reading `epic:audit` does inside one epic. That analysis is imported rather than
16
+ // written a second time; the only difference is what it is pointed at.
17
17
 
18
18
  // An open issue as the search sees it.
19
19
  interface BacklogIssue {
@@ -59,11 +59,19 @@ interface BundleDecision {
59
59
  reason: string
60
60
  }
61
61
 
62
- // Whether `from`'s body names `to`. One direction of the citation test, because which direction it
63
- // was is what `epic-bundle-evidence.ts` reports — and the read is single-sourced here rather than
64
- // written a second time there, so the candidate search and the evidence beside it cannot drift apart.
62
+ // Where `from`'s body names `to` in a place that counts as a relation — its acceptance criteria or
63
+ // its `parent: #N` line — or nothing. One direction of the citation test, because which direction
64
+ // and which section it was is what `epic-bundle-evidence.ts` reports — and the read is single-sourced
65
+ // here rather than written a second time there, so the candidate search and the evidence beside it
66
+ // cannot drift apart. A number written only as the background of a filing is not a relation.
67
+ function reference_section(from: BacklogIssue, to: BacklogIssue): SignalSection | undefined {
68
+ return epic_audit_logic
69
+ .signal_references(from.body, from.repo)
70
+ .find((reference) => reference.number === to.number)?.section
71
+ }
72
+
65
73
  function names_in_body(from: BacklogIssue, to: BacklogIssue): boolean {
66
- return epic_audit_logic.parse_references(from.body, from.repo).includes(to.number)
74
+ return reference_section(from, to) !== undefined
67
75
  }
68
76
 
69
77
  // Whether two issues cite each other, in either direction. A one-way citation is enough: the point
@@ -324,7 +332,7 @@ function bundle_children(
324
332
 
325
333
  const epic_bundle = {
326
334
  names_as_blocker,
327
- names_in_body,
335
+ reference_section,
328
336
  NO_SIGNAL_REASON,
329
337
  already_tracked_reason,
330
338
  has_recorded_dependency,
@@ -190,8 +190,12 @@ function find_position_ambiguity(
190
190
  chains: Chains,
191
191
  position: InsertPosition | undefined,
192
192
  ): string | undefined {
193
- if (position?.kind !== 'before') return undefined
194
- if (chains_containing(chains, position.target).length < AMBIGUOUS_MATCH_COUNT) return undefined
193
+ if (
194
+ position?.kind !== 'before' ||
195
+ chains_containing(chains, position.target).length < AMBIGUOUS_MATCH_COUNT
196
+ ) {
197
+ return undefined
198
+ }
195
199
 
196
200
  return to_ambiguous_position_error(position)
197
201
  }
@@ -66,8 +66,7 @@ function marker_children(line: string): Array<string> | undefined {
66
66
  if (indent_of(line) >= INDENTED_CODE_SPACES) return undefined
67
67
 
68
68
  const trimmed = line.trim()
69
- if (!trimmed.startsWith(MARKER_OPEN)) return undefined
70
- if (!trimmed.endsWith(MARKER_CLOSE)) return undefined
69
+ if (!trimmed.startsWith(MARKER_OPEN) || !trimmed.endsWith(MARKER_CLOSE)) return undefined
71
70
 
72
71
  const listed = trimmed.slice(MARKER_OPEN.length, -MARKER_CLOSE.length)
73
72
 
@@ -7,6 +7,7 @@ import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
7
7
  import { cli_body } from '#scripts/josh/cli-body'
8
8
  import { josh_environment_file } from '#scripts/josh/josh-environment-file'
9
9
  import { git_notify, type GitNotifyConfig } from '#scripts/notify/git-notify'
10
+ import { acceptance_map_check } from '#scripts/review/acceptance-map-check'
10
11
  import { live_evidence } from '#scripts/review/live-evidence'
11
12
  import { review_attest } from '#scripts/review/review-attest'
12
13
  import { review_record } from '#scripts/review/review-record'
@@ -143,7 +144,7 @@ function is_merge_permitted(status: string): boolean {
143
144
  return status === 'ok' || status === 'not-required'
144
145
  }
145
146
 
146
- function is_valid_issue(issue_number: string | undefined): boolean {
147
+ function is_valid_issue(issue_number: string | undefined): issue_number is string {
147
148
  return issue_number !== undefined && issue_number_shape.ISSUE_NUMBER_PATTERN.test(issue_number)
148
149
  }
149
150
 
@@ -194,6 +195,21 @@ async function assert_live_evidence(should_merge: boolean, branch_name: string):
194
195
  throw new Error(live_evidence.refusal_message())
195
196
  }
196
197
 
198
+ // Every acceptance criterion the Issue carries at the merge maps to a row in the pull request body —
199
+ // `acceptance_map` carries the why. Re-read here rather than at the commit, so a criterion added
200
+ // during the review is refused too. A run whose issue it cannot identify has no criteria to read.
201
+ async function assert_acceptance_mapped(
202
+ should_merge: boolean,
203
+ issue_number: string | undefined,
204
+ branch_name: string,
205
+ ): Promise<void> {
206
+ if (!should_merge || !is_valid_issue(issue_number)) return
207
+
208
+ const refusal = await acceptance_map_check.refusal(issue_number, branch_name)
209
+
210
+ if (refusal !== undefined) throw new Error(refusal)
211
+ }
212
+
197
213
  // Every pre-merge refusal, in order — each one throws, so nothing merges past the first that fails.
198
214
  async function assert_merge_gates(
199
215
  should_merge: boolean,
@@ -203,6 +219,7 @@ async function assert_merge_gates(
203
219
  await assert_review_attested(should_merge)
204
220
  await assert_review_recorded(should_merge, issue_number)
205
221
  await assert_live_evidence(should_merge, branch_name)
222
+ await assert_acceptance_mapped(should_merge, issue_number, branch_name)
206
223
  }
207
224
 
208
225
  // After the gates, before the CI wait: a line appended since the run's commit rides the pull request
@@ -261,6 +278,7 @@ try {
261
278
  }
262
279
 
263
280
  const git_followup_workflow = {
281
+ assert_acceptance_mapped,
264
282
  assert_live_evidence,
265
283
  assert_review_attested,
266
284
  assert_review_recorded,
@@ -0,0 +1,129 @@
1
+ import path from 'node:path'
2
+ import { lint_related_scope } from '#scripts/lint/lint-related-scope'
3
+ import { gate_plan } from './gate-plan'
4
+
5
+ // Which paths each gate check reads, so a gate re-run reuses a check none of whose inputs moved
6
+ // (`check-reuse.ts`). **A check with no scope here always runs**: `cspell` reads every
7
+ // text file, the unit suite reads documents and fixtures as well as code, `behavior` reads a transcript
8
+ // outside the tree and `metrics` reads whatever it measures. Naming a scope for any of them would be a
9
+ // guess, and a wrong guess is a check reported green without having run.
10
+ //
11
+ // **The scopes over-approximate, never under**: a path in doubt is an input. `check` and `exports`
12
+ // read source modules and the JSON they import. `lint` is prettier plus eslint, so its inputs are those
13
+ // sources plus every file prettier formats — the extensions `lint:related` already selects, less what
14
+ // prettier's own ignore files leave out (`.claude/**/*.md`). Prettier answers that itself through
15
+ // `getFileInfo`, so the two ignore files stay the single source.
16
+ //
17
+ // **A configuration file is an input of every scoped check**: a manifest or lock file moves the
18
+ // installed toolchain, and an ignore or editor file moves what prettier reads.
19
+
20
+ const SOURCE_EXTENSIONS: ReadonlySet<string> = new Set([
21
+ '.ts',
22
+ '.tsx',
23
+ '.mts',
24
+ '.cts',
25
+ '.js',
26
+ '.jsx',
27
+ '.mjs',
28
+ '.cjs',
29
+ '.svelte',
30
+ '.vue',
31
+ '.json',
32
+ '.jsonc',
33
+ ])
34
+
35
+ const GIT_IGNORE = '.gitignore'
36
+ const PRETTIER_IGNORE = '.prettierignore'
37
+ const PRETTIER_IGNORE_FILES = [GIT_IGNORE, PRETTIER_IGNORE] as const
38
+
39
+ const CONFIG_FILES: ReadonlySet<string> = new Set([
40
+ 'package.json',
41
+ 'pnpm-lock.yaml',
42
+ 'pnpm-workspace.yaml',
43
+ 'package-lock.json',
44
+ 'yarn.lock',
45
+ 'bun.lock',
46
+ 'bun.lockb',
47
+ '.npmrc',
48
+ '.editorconfig',
49
+ ...PRETTIER_IGNORE_FILES,
50
+ ])
51
+
52
+ const PRETTIER_CONFIG_PREFIX = '.prettierrc'
53
+
54
+ // Whether prettier would leave `relative` out; `false` when prettier cannot be asked, which keeps the
55
+ // path an input.
56
+ type PrettierIgnored = (relative: string) => Promise<boolean>
57
+
58
+ type InputTest = (relative: string, is_ignored: PrettierIgnored) => Promise<boolean>
59
+
60
+ function is_config(relative: string): boolean {
61
+ const name = path.basename(relative)
62
+
63
+ return CONFIG_FILES.has(name) || name.startsWith(PRETTIER_CONFIG_PREFIX)
64
+ }
65
+
66
+ function is_source(relative: string): boolean {
67
+ return SOURCE_EXTENSIONS.has(path.extname(relative))
68
+ }
69
+
70
+ async function is_lint_input(relative: string, is_ignored: PrettierIgnored): Promise<boolean> {
71
+ if (is_source(relative)) return true
72
+ if (!lint_related_scope.is_lintable(relative)) return false
73
+
74
+ return !(await is_ignored(relative))
75
+ }
76
+
77
+ async function is_source_input(relative: string): Promise<boolean> {
78
+ return is_source(relative)
79
+ }
80
+
81
+ function prettier_ignored(root: string): PrettierIgnored {
82
+ const ignore_path = PRETTIER_IGNORE_FILES.map((name) => path.join(root, name))
83
+
84
+ return async function is_ignored(relative: string): Promise<boolean> {
85
+ try {
86
+ const prettier = await import('prettier')
87
+ const info = await prettier.getFileInfo(path.join(root, relative), {
88
+ ignorePath: ignore_path,
89
+ })
90
+
91
+ return info.ignored
92
+ } catch {
93
+ return false
94
+ }
95
+ }
96
+ }
97
+
98
+ const SCOPES: ReadonlyMap<string, InputTest> = new Map([
99
+ [gate_plan.LINT_LABEL, is_lint_input],
100
+ [gate_plan.TYPE_CHECK_LABEL, is_source_input],
101
+ [gate_plan.EXPORTS_LABEL, is_source_input],
102
+ ])
103
+
104
+ function is_scoped(label: string): boolean {
105
+ return SCOPES.has(label)
106
+ }
107
+
108
+ // Whether any of `paths` (repository-root relative) is an input of the check `label`. An unscoped
109
+ // check answers `true` for every set of paths, so the only way to reuse it is never.
110
+ async function touches_inputs(
111
+ label: string,
112
+ paths: ReadonlyArray<string>,
113
+ is_ignored: PrettierIgnored,
114
+ ): Promise<boolean> {
115
+ const scope = SCOPES.get(label)
116
+
117
+ if (scope === undefined || paths.some((relative) => is_config(relative))) return true
118
+
119
+ const answers = await Promise.all(
120
+ paths.map(async (relative) => await scope(relative, is_ignored)),
121
+ )
122
+
123
+ return answers.includes(true)
124
+ }
125
+
126
+ const check_inputs = { is_scoped, prettier_ignored, touches_inputs }
127
+
128
+ export type { PrettierIgnored }
129
+ export { check_inputs }
@@ -0,0 +1,162 @@
1
+ import { git_command } from '#scripts/git/git-command'
2
+ import { git_spawn } from '#scripts/git/git-spawn'
3
+ import { PROJECT_ROOT } from '#scripts/init/init-paths'
4
+ import { file_map_stamp, type FileMapStamp } from '#scripts/josh/file-map-stamp'
5
+ import { stamp_file } from '#scripts/josh/stamp-file'
6
+ import { review_tree } from '#scripts/review/review-tree'
7
+ import { check_inputs, type PrettierIgnored } from './check-inputs'
8
+ import { gate_report, type GateStepResult } from './gate-report'
9
+ import type { GateTree } from './gate-tree'
10
+
11
+ // One green record per scoped check, so a gate re-run starts only the checks whose inputs moved
12
+ // (`check-inputs.ts` decides what each reads). The whole-gate record (`gate-skip.ts`) is refused the
13
+ // moment the merge base moves; a main sync that brought in only `.claude/**/*.md` then re-ran lint and
14
+ // the type check over sources neither had a reason to read again.
15
+ //
16
+ // **What moved is the union of two diffs**, and together they are every path whose content can differ
17
+ // from the tree the record was taken on. The changed-file map is a diff against the merge base, so a
18
+ // path absent from both maps holds the merge base's content — and the base's own diff
19
+ // (`git diff <recorded> <current>`) names every path that content moved for. A path in either map whose
20
+ // digest differs is the other half (`file_map_stamp.changed_since`).
21
+ //
22
+ // **A record is reuse of a result, not a check dropped**: no base, an empty changed-file map, an
23
+ // unreadable base diff or no root answers "not reusable", and a check with no scope is never reusable. The records are written and read
24
+ // only where the CLI entry resolved their destination (`CheckRecords`), so a suite driving the gate
25
+ // never plants a green record over the live tree.
26
+
27
+ const CHECK_PREFIX = 'josh-gate-check-'
28
+ const NAME_SEPARATOR = '\0'
29
+ const BASE_DIFF = ['diff', '--name-only', '--no-renames', '-z'] as const
30
+
31
+ const check_stamp = file_map_stamp.create(CHECK_PREFIX, PROJECT_ROOT)
32
+
33
+ // Where each check's record lives — a parameter so a test plants records in a directory of its own.
34
+ type CheckRecords = (label: string) => string
35
+
36
+ function default_records(label: string): string {
37
+ return stamp_file.stamp_path(`${CHECK_PREFIX}${label}-`, PROJECT_ROOT)
38
+ }
39
+
40
+ async function base_diff(from: string, to: string): Promise<ReadonlyArray<string> | undefined> {
41
+ if (from === to) return []
42
+
43
+ try {
44
+ const names = await git_spawn.read([...BASE_DIFF, from, to])
45
+
46
+ return names.split(NAME_SEPARATOR).filter((name) => name !== '')
47
+ } catch {
48
+ return undefined
49
+ }
50
+ }
51
+
52
+ async function moved_since(
53
+ stamp: FileMapStamp,
54
+ tree: GateTree,
55
+ ): Promise<ReadonlyArray<string> | undefined> {
56
+ // An empty map is also what `gate-tree.ts` answers when git could not be read, so it describes no
57
+ // tree to diff against — the reading `gate_skip.gate_reuse` refuses too.
58
+ const is_unread = Object.keys(tree.files).length === 0
59
+
60
+ if (is_unread || stamp.base === undefined || tree.base === undefined) return undefined
61
+
62
+ const moved_base = await base_diff(stamp.base, tree.base)
63
+
64
+ if (moved_base === undefined) return undefined
65
+
66
+ return [...file_map_stamp.changed_since(stamp, tree.files), ...moved_base]
67
+ }
68
+
69
+ interface ReuseContext {
70
+ tree: GateTree
71
+ records: CheckRecords
72
+ is_ignored: PrettierIgnored
73
+ }
74
+
75
+ async function is_stamp_reusable(
76
+ label: string,
77
+ stamp: FileMapStamp,
78
+ context: ReuseContext,
79
+ ): Promise<boolean> {
80
+ const moved = await moved_since(stamp, context.tree)
81
+
82
+ if (moved === undefined) return false
83
+
84
+ return !(await check_inputs.touches_inputs(label, moved, context.is_ignored))
85
+ }
86
+
87
+ // The time the check's record was taken, or `undefined` when the check has to run.
88
+ async function reused_at(label: string, context: ReuseContext): Promise<string | undefined> {
89
+ const stamp = check_inputs.is_scoped(label) ? check_stamp.read(context.records(label)) : undefined
90
+
91
+ if (stamp === undefined) return undefined
92
+
93
+ return (await is_stamp_reusable(label, stamp, context)) ? stamp.taken_at : undefined
94
+ }
95
+
96
+ async function read_root(): Promise<string | undefined> {
97
+ try {
98
+ return await git_command.repository_root()
99
+ } catch {
100
+ return undefined
101
+ }
102
+ }
103
+
104
+ // Each reusable check's label, mapped to when it was last green.
105
+ async function reusable_checks(
106
+ labels: ReadonlyArray<string>,
107
+ tree: GateTree,
108
+ records: CheckRecords = default_records,
109
+ ): Promise<ReadonlyMap<string, string>> {
110
+ const root = await read_root()
111
+
112
+ if (root === undefined) return new Map()
113
+
114
+ const context = { tree, records, is_ignored: check_inputs.prettier_ignored(root) }
115
+ const answers = await Promise.all(
116
+ labels.map(async (label) => [label, await reused_at(label, context)] as const),
117
+ )
118
+
119
+ return new Map(answers.flatMap(([label, at]) => (at === undefined ? [] : [[label, at] as const])))
120
+ }
121
+
122
+ function is_recordable(result: GateStepResult): boolean {
123
+ return (
124
+ check_inputs.is_scoped(result.label) &&
125
+ !gate_report.is_gate_step_failed(result) &&
126
+ gate_report.has_nothing_to_say(result)
127
+ )
128
+ }
129
+
130
+ function write_records(
131
+ results: ReadonlyArray<GateStepResult>,
132
+ tree: GateTree,
133
+ records: CheckRecords,
134
+ ): void {
135
+ const recordable = results.filter((result) => is_recordable(result))
136
+
137
+ for (const result of recordable) check_stamp.write(tree.files, records(result.label), tree.base)
138
+ }
139
+
140
+ // Every scoped check that passed silently, recorded against the tree it read — a red gate's green
141
+ // checks included, since each record answers for its own check alone. Nothing is written when the tree
142
+ // moved while the checks ran, and a failed write leaves no record, the safe direction either way.
143
+ async function record_green_checks(
144
+ results: ReadonlyArray<GateStepResult>,
145
+ tree: GateTree,
146
+ records: CheckRecords = default_records,
147
+ ): Promise<void> {
148
+ try {
149
+ const after = await review_tree.read_changed_tree()
150
+
151
+ if (JSON.stringify(after) !== JSON.stringify(tree.files)) return
152
+
153
+ write_records(results, { files: after, base: tree.base }, records)
154
+ } catch {
155
+ /* no record is the safe answer */
156
+ }
157
+ }
158
+
159
+ const check_reuse = { DEFAULT_RECORDS: default_records, record_green_checks, reusable_checks }
160
+
161
+ export type { CheckRecords }
162
+ export { check_reuse }