@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
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env tsx
2
+ import { fileURLToPath } from 'node:url'
3
+ import { core_budget } from '#scripts/gate/core-budget'
4
+ import {
5
+ CSPELL_COMMIT_CACHE_FLAGS,
6
+ ESLINT_COMMIT_CACHE_FLAGS,
7
+ PRETTIER_CACHE_FLAGS,
8
+ } from '#scripts/josh/josh-command-types'
9
+ import { resolve_spawn_exit } from '#scripts/lib/spawn-exit'
10
+ import { execa } from 'execa'
11
+
12
+ // The pre-commit hook's cspell, prettier and eslint runs over the staged files. They go through here
13
+ // rather than a bare `pnpm exec` in `lefthook/base.yml` so each cache location comes from the
14
+ // constant its ignore rules are asserted against (joshuafolkken/kit#3948) — a YAML copy would keep
15
+ // writing the old file the day that constant moves.
16
+ //
17
+ // cspell and eslint write the hook's own files: a staged-files cspell run prunes every entry it did
18
+ // not visit, and `--quiet` stores eslint reports without their warnings. Prettier does neither, so
19
+ // it shares the gate's file and a commit right after a green gate checks nothing twice.
20
+
21
+ const ARGV_OFFSET = 2
22
+ const USAGE_ERROR_EXIT_CODE = 2
23
+ const PNPM = 'pnpm'
24
+
25
+ const TOOL_ARGUMENTS: Readonly<Record<string, ReadonlyArray<string>>> = {
26
+ cspell: [
27
+ 'exec',
28
+ 'cspell',
29
+ 'lint',
30
+ '--no-must-find-files',
31
+ '--no-progress',
32
+ ...CSPELL_COMMIT_CACHE_FLAGS,
33
+ ],
34
+ prettier: ['exec', 'prettier', '--check', '--ignore-unknown', ...PRETTIER_CACHE_FLAGS],
35
+ eslint: ['exec', 'eslint', '--quiet', ...ESLINT_COMMIT_CACHE_FLAGS],
36
+ }
37
+
38
+ // **Each tool claims its place in the core budget** (joshuafolkken/kit#3944): lefthook starts the three
39
+ // in parallel, and off the ledger every other lane's admission saw them only as foreign load. Each is
40
+ // one process over the staged files — eslint over 40 of them took 16.9s of CPU over 14.0s of wall at
41
+ // 1,225 MB, the one-process shape the whole-tree scan's weight already names.
42
+ // The key is the one `CORE_WEIGHTS` and `MEMORY_MB` both declare the tool under.
43
+ const TOOL_BUDGETS: Readonly<Record<string, 'spell_check' | 'format' | 'eslint_scan'>> = {
44
+ cspell: 'spell_check',
45
+ prettier: 'format',
46
+ eslint: 'eslint_scan',
47
+ }
48
+
49
+ const USAGE = `Usage: josh pre-commit-lint <${Object.keys(TOOL_ARGUMENTS).join('|')}> [files...]`
50
+
51
+ function build_arguments(
52
+ tool: string,
53
+ staged_files: ReadonlyArray<string>,
54
+ ): ReadonlyArray<string> | undefined {
55
+ const tool_arguments = TOOL_ARGUMENTS[tool]
56
+ if (tool_arguments === undefined) return undefined
57
+
58
+ return [...tool_arguments, ...staged_files]
59
+ }
60
+
61
+ async function run_tool(command_arguments: ReadonlyArray<string>): Promise<number> {
62
+ const result = await execa(PNPM, command_arguments, { stdio: 'inherit', reject: false })
63
+
64
+ return resolve_spawn_exit(PNPM, result)
65
+ }
66
+
67
+ async function run(argv: ReadonlyArray<string>): Promise<number> {
68
+ const [tool = '', ...staged_files] = argv
69
+ const command_arguments = build_arguments(tool, staged_files)
70
+ const budget = TOOL_BUDGETS[tool]
71
+
72
+ if (command_arguments === undefined || budget === undefined) {
73
+ console.error(USAGE)
74
+
75
+ return USAGE_ERROR_EXIT_CODE
76
+ }
77
+
78
+ return await core_budget.with_command_reservation(
79
+ core_budget.CORE_WEIGHTS[budget],
80
+ async () => await run_tool(command_arguments),
81
+ { memory_mb: core_budget.MEMORY_MB[budget], command: `pre-commit ${tool}` },
82
+ )
83
+ }
84
+
85
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
86
+ process.exitCode = await run(process.argv.slice(ARGV_OFFSET))
87
+ }
88
+
89
+ const pre_commit_lint = { build_arguments, run, USAGE_ERROR_EXIT_CODE }
90
+
91
+ export { pre_commit_lint }
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
+ import { core_budget } from '#scripts/gate/core-budget'
4
+ import { gate_plan } from '#scripts/gate/gate-plan'
3
5
  import { gate_skip, type GateReuse } from '#scripts/gate/gate-skip'
4
6
  import { gate_tree, type GateTree } from '#scripts/gate/gate-tree'
5
7
  import { hook_gate_reuse } from '#scripts/gate/hook-gate-reuse'
@@ -123,6 +125,18 @@ function format_miss(reason: string): string {
123
125
  return `↻ running the whole unit suite — the green gate record does not cover this push: ${reason}.`
124
126
  }
125
127
 
128
+ // **The whole suite claims its place in the core budget, the one `josh test:unit` claims.** Without it
129
+ // a push's suite ran beside other lanes' gates off the ledger, so their admission saw its workers only
130
+ // as foreign load (joshuafolkken/kit#3874). Only the miss reserves: a reused record runs nothing, and a
131
+ // claim there could make a push wait out a full ledger for a suite it then skips.
132
+ async function run_reserved_suite(vitest_arguments: ReadonlyArray<string>): Promise<number> {
133
+ return await core_budget.with_command_reservation(
134
+ gate_plan.direct_unit_weight(),
135
+ async () => await test_unit_guard.run_guarded_unit(process.cwd(), vitest_arguments),
136
+ { memory_mb: gate_plan.direct_unit_memory(), command: 'pre-push test:unit' },
137
+ )
138
+ }
139
+
126
140
  async function run_pre_push_unit(
127
141
  extra_arguments: ReadonlyArray<string> = [],
128
142
  source?: string,
@@ -132,10 +146,7 @@ async function run_pre_push_unit(
132
146
  if ('miss' in reuse) {
133
147
  process.stdout.write(`${format_miss(reuse.miss)}\n`)
134
148
 
135
- return await test_unit_guard.run_guarded_unit(
136
- process.cwd(),
137
- forwarded_arguments(extra_arguments),
138
- )
149
+ return await run_reserved_suite(forwarded_arguments(extra_arguments))
139
150
  }
140
151
 
141
152
  process.stdout.write(`${format_skip(reuse.stamp.taken_at)}\n`)
@@ -1,13 +1,13 @@
1
1
  import { duplicate_read_outcome } from '#scripts/delegation/duplicate-read-guard'
2
- import { investigation_refusal } from '#scripts/delegation/investigation-guard'
2
+ import { investigation_outcome } from '#scripts/delegation/investigation-guard'
3
3
  import { issue_watch_hook } from '#scripts/issue/issue-watch-hook'
4
4
  import { hook_decision, type GuardOutcome } from '#scripts/josh/hook-decision'
5
5
  import { lane_phase } from '#scripts/lane/lane-phase'
6
- import { json_value } from '#scripts/lib/json-value'
7
6
  import { delivered_rules } from '#scripts/rules/delivered-rules'
8
7
  import { lane_background } from '#scripts/rules/lane-background'
9
- import { piped_verification } from '#scripts/rules/piped-verification'
8
+ import { rule_rewrite, type RuleRewrite } from '#scripts/rules/rule-rewrite'
10
9
  import { run_parent_cut_hook } from '#scripts/run/run-parent-cut-hook'
10
+ import { run_poll_hook } from '#scripts/run/run-poll-hook'
11
11
  import { run_watcher_hook } from '#scripts/run/run-watcher-hook'
12
12
  import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
13
13
  import { batch_outcome } from './batch-guard'
@@ -22,38 +22,34 @@ import { step_zero_notice } from './step-zero-notice'
22
22
  //
23
23
  // **The verdict is identical to running the three in turn**, and that is the whole point. Each guard
24
24
  // self-gates on the tool name inside its own `is_candidate` / trigger, so running all three on the
25
- // union matcher (`Bash|Edit|Read|Write|AskUserQuestion`) refuses exactly what the three separate
26
- // entries did — `AskUserQuestion` is matched for the rule guard's lane-child interactive-ask row,
27
- // and the batching and investigation guards self-gate away from it. A
28
- // refusal from any guard wins first; only when none refuses is the batch guard's non-blocking notice
29
- // (the whole-file-write notice) emitted. Each guard still honours its own
25
+ // union matcher (`Bash|Edit|Read|Write|AskUserQuestion|Agent|Task`) refuses exactly what the three
26
+ // separate entries did — `AskUserQuestion` is matched for the rule guard's lane-child interactive-ask
27
+ // row and `Agent|Task` for its `agent-model` row, and the batching and investigation guards self-gate
28
+ // away from both. A refusal from any guard wins first; only when none refuses is a guard's non-blocking
29
+ // notice or fault (the whole-file-write notice, a lane child's duplicate-read notice) emitted. Each
30
+ // guard still honours its own
30
31
  // switch (`JOSH_BATCH_GUARD` / `JOSH_INVESTIGATION_GUARD` / `JOSH_RULE_GUARD`) internally, so this
31
32
  // process needs no switch of its own.
32
33
 
33
- // Fold the guards' verdicts into one. A refusal wins over everything else, and the batch guard's
34
- // reason is shown first to match the entry order the separate hooks had. When nothing refuses,
35
- // whatever the batch guard reported (a notice, a fault, or nothing) passes through untouched.
36
- function combine_outcomes(batch: GuardOutcome, extra_reason: string | undefined): GuardOutcome {
37
- if (batch.reason !== undefined) return batch
38
-
39
- if (extra_reason !== undefined) {
40
- return { reason: extra_reason, notice: undefined, fault: undefined }
41
- }
42
-
43
- return batch
44
- }
34
+ const CLEAR: GuardOutcome = { reason: undefined, notice: undefined, fault: undefined }
45
35
 
46
36
  function is_clear(outcome: GuardOutcome): boolean {
47
37
  return outcome.reason === undefined && outcome.notice === undefined && outcome.fault === undefined
48
38
  }
49
39
 
50
- // The duplicate-read guard is `notice`-mode in a lane child, so it is the second guard that can raise a
51
- // non-refusal notice. Its notice is surfaced only where nothing else spoke —
52
- // a refusal from any guard, or the batch guard's own notice, wins first.
53
- function with_duplicate_notice(combined: GuardOutcome, notice: string | undefined): GuardOutcome {
54
- if (notice === undefined || !is_clear(combined)) return combined
40
+ // Fold the guards' verdicts into one. A refusal wins over everything else, in the entry order the
41
+ // separate hooks had: the outcomes in the order given, then the rule guard's reason. When nothing
42
+ // refuses, the first guard that spoke passes its notice or fault through untouched — so a lane child's
43
+ // `notice`-mode guidance reaches the model rather than only spending its stamp (joshuafolkken/kit#3877).
44
+ function combine_outcomes(
45
+ outcomes: ReadonlyArray<GuardOutcome>,
46
+ rule_reason: string | undefined,
47
+ ): GuardOutcome {
48
+ const reason = outcomes.find((outcome) => outcome.reason !== undefined)?.reason ?? rule_reason
49
+
50
+ if (reason !== undefined) return { reason, notice: undefined, fault: undefined }
55
51
 
56
- return { reason: undefined, notice, fault: undefined }
52
+ return outcomes.find((outcome) => !is_clear(outcome)) ?? CLEAR
57
53
  }
58
54
 
59
55
  // The Step 0 reminder is the last word: asked only where every guard stayed
@@ -70,14 +66,13 @@ function with_step_zero_notice(outcome: GuardOutcome, raw_payload: string): Guar
70
66
  // once-per-run stamp, so short-circuiting on the first refusal would change which guard is allowed to
71
67
  // fire on a later call.
72
68
  function guard_outcome(raw_payload: string): GuardOutcome {
73
- const batch = batch_outcome(raw_payload)
74
- const investigation = investigation_refusal(raw_payload)
75
- const duplicate = duplicate_read_outcome(raw_payload)
76
- const rule = delivered_rules.delivery(raw_payload)
77
- const duplicate_refusal = duplicate.reason
78
- const combined = combine_outcomes(batch, investigation ?? duplicate_refusal ?? rule)
79
-
80
- return with_duplicate_notice(combined, duplicate.notice)
69
+ const outcomes = [
70
+ batch_outcome(raw_payload),
71
+ investigation_outcome(raw_payload),
72
+ duplicate_read_outcome(raw_payload),
73
+ ]
74
+
75
+ return combine_outcomes(outcomes, delivered_rules.delivery(raw_payload))
81
76
  }
82
77
 
83
78
  // The synchronous verdict the Codex adapter asks for, with the Step 0 reminder as its last layer.
@@ -102,12 +97,14 @@ async function mark_phase(outcome: GuardOutcome): Promise<void> {
102
97
  // **The parent hand-off guard is the second asynchronous rule, asked after the watcher's**.
103
98
  // It reads the carry record to tell the `backlogrun` parent apart, so it sits
104
99
  // beside the watcher rather than among the synchronous rows; a stale watcher is the cheaper fix and is
105
- // surfaced first.
100
+ // surfaced first. **The poll guard is third**: it reads the same carry record, and only for a call that
101
+ // repeats a state read, so the hand-off still wins over it.
106
102
  async function async_reason(raw_payload: string): Promise<string | undefined> {
107
103
  const watcher = await run_watcher_hook.watcher_hook_reason(raw_payload)
108
104
  const cut = watcher ?? (await run_parent_cut_hook.parent_cut_reason(raw_payload))
105
+ const poll = cut ?? (await run_poll_hook.safe_poll_reason(raw_payload))
109
106
 
110
- return cut ?? (await issue_watch_hook.watch_reason())
107
+ return poll ?? (await issue_watch_hook.watch_reason())
111
108
  }
112
109
 
113
110
  async function pretool_outcome_async(raw_payload: string): Promise<GuardOutcome> {
@@ -165,59 +162,32 @@ function foreground_rewrite(
165
162
  )
166
163
  }
167
164
 
168
- interface PipefailRewrite {
169
- payload: string
170
- input: Record<string, unknown>
171
- }
172
-
173
- // A piped josh check the hook runs under `set -o pipefail` instead of refusing it:
174
- // the rewritten input, and the payload that carries it. The rewrite is the
175
- // rule guard's delivery, so the rule guard's switch turns it off too.
176
- function pipefail_rewrite(raw_payload: string): PipefailRewrite | undefined {
177
- if (!delivered_rules.is_enabled()) return undefined
178
-
179
- const call = guarded_call(raw_payload)
180
- const input = call === undefined ? undefined : piped_verification.pipefail_input(call)
181
- const payload = json_value.parse_or_undefined(raw_payload)
182
-
183
- if (input === undefined || !json_value.is_record(payload)) return undefined
184
-
185
- return { payload: JSON.stringify({ ...payload, tool_input: input }), input }
186
- }
187
-
188
- function pipefail_envelope(
165
+ // The rows' rewrite carries no permission decision: whatever else the line runs meets its own prompt.
166
+ function rule_envelope(
189
167
  outcome: GuardOutcome,
190
- input: Record<string, unknown> | undefined,
168
+ rewrite: RuleRewrite | undefined,
191
169
  ): string | undefined {
192
- if (input === undefined || outcome.reason !== undefined) return undefined
170
+ if (rewrite === undefined || outcome.reason !== undefined) return undefined
193
171
 
194
- return hook_decision.rewrite_envelope(
195
- input,
196
- rewrite_context(piped_verification.PIPEFAIL_NOTE, outcome),
197
- )
172
+ return hook_decision.flow_rewrite_envelope(rewrite.input, rewrite_context(rewrite.note, outcome))
198
173
  }
199
174
 
200
175
  // The hook's one write: a rewrite where one applies, the outcome's own envelope otherwise.
201
- function emit(
202
- raw_payload: string,
203
- outcome: GuardOutcome,
204
- pipefail_input?: Record<string, unknown>,
205
- ): void {
206
- const rewrite =
207
- pipefail_envelope(outcome, pipefail_input) ?? foreground_rewrite(outcome, raw_payload)
176
+ function emit(raw_payload: string, outcome: GuardOutcome, rewrite?: RuleRewrite): void {
177
+ const envelope = rule_envelope(outcome, rewrite) ?? foreground_rewrite(outcome, raw_payload)
208
178
 
209
- if (rewrite === undefined) hook_decision.emit_outcome(outcome)
210
- else process.stdout.write(`${rewrite}\n`)
179
+ if (envelope === undefined) hook_decision.emit_outcome(outcome)
180
+ else process.stdout.write(`${envelope}\n`)
211
181
  }
212
182
 
213
- // **The guards judge the call that will run.** A rewritable piped check reaches every guard already
214
- // prefixed, so the piped-verification row stays quiet on it while any other guard's refusal still wins
215
- // over the rewrite. The Codex adapter cannot rewrite a call, so it goes on judging the call as typed.
183
+ // **The guards judge the call that will run.** A rewritten call reaches every guard already made
184
+ // right, so the rewriting row stays quiet on it while any other guard's refusal still wins over the
185
+ // rewrite. The Codex adapter cannot rewrite a call, so it goes on judging the call as typed.
216
186
  async function respond(raw_payload: string): Promise<void> {
217
- const pipefail = pipefail_rewrite(raw_payload)
218
- const judged = pipefail?.payload ?? raw_payload
187
+ const rewrite = rule_rewrite.rewrite(raw_payload)
188
+ const judged = rewrite?.payload ?? raw_payload
219
189
 
220
- emit(judged, await pretool_outcome_async(judged), pipefail?.input)
190
+ emit(judged, await pretool_outcome_async(judged), rewrite)
221
191
  }
222
192
 
223
193
  const pretool_guard = {
@@ -225,7 +195,6 @@ const pretool_guard = {
225
195
  emit,
226
196
  foreground_rewrite,
227
197
  mark_phase,
228
- pipefail_rewrite,
229
198
  pretool_outcome,
230
199
  respond,
231
200
  }
@@ -78,6 +78,7 @@ async function run_reserved(argv: ReadonlyArray<string>): Promise<number> {
78
78
  return await core_budget.with_core_reservation(
79
79
  parsed.weight,
80
80
  async () => await run_command(parsed.command),
81
+ { command: parsed.command.join(' ') },
81
82
  )
82
83
  }
83
84
 
@@ -5,9 +5,8 @@
5
5
  # A hook command in `.claude/settings.json` / `.codex/hooks.json` only moves to the project root
6
6
  # (`scripts/init/hook-launch.ts`) and calls this script, so everything every hook shares lives here once:
7
7
  #
8
- # - The git location variables a git hook exports are cleared when the root resolves without them, so
9
- # the hook's own git calls act on the checkout it stands in. They are kept when the checkout needs
10
- # them — git metadata outside the work tree.
8
+ # - The root prefix has already cleared the git location variables a git hook exports, or kept them
9
+ # when the checkout needs them; this script starts no `git` of its own (joshuafolkken/kit#3946).
11
10
  # - The pre-built bundle `dist/hooks/<name>.js` runs once its gate passes; otherwise the live source
12
11
  # runs through josh, whose command is the name with its first `-` turned into `:`
13
12
  # (`pretool-guard` → `pretool:guard`).
@@ -16,27 +15,26 @@
16
15
  # starts node once (joshuafolkken/kit#3327). In an installed package it is a presence check: the
17
16
  # published bundles cannot be stale against a source the consumer never edits, and Node strips no
18
17
  # types under `node_modules`.
18
+ # - Nothing is spawned that the shell can do itself: the directory is a parameter expansion rather than
19
+ # `dirname`, and `node` replaces this shell through `exec` rather than running beside it.
19
20
  # - An `if`/`else`, never `&&`/`||`: a guard's refusal is a non-zero exit, and a chain would read it as
20
21
  # a missing bundle and run the hook twice.
21
22
 
22
- location_variables='GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR GIT_DIR GIT_INDEX_FILE GIT_NAMESPACE GIT_OBJECT_DIRECTORY GIT_WORK_TREE'
23
- unset_options=''
24
- for variable in $location_variables; do unset_options="$unset_options -u $variable"; done
25
- # shellcheck disable=SC2086
26
- if env $unset_options git rev-parse --show-toplevel >/dev/null 2>&1; then unset $location_variables; fi
27
-
28
23
  hook_name=$1
29
24
  shift
30
- hooks_dir=$(dirname "$0")
25
+ case $0 in
26
+ */*) hooks_dir=${0%/*} ;;
27
+ *) hooks_dir=. ;;
28
+ esac
31
29
  package_dir="$hooks_dir/../.."
32
30
  bundle="$package_dir/dist/hooks/$hook_name.js"
33
31
  josh_command="${hook_name%%-*}:${hook_name#*-}"
34
32
 
35
33
  case $0 in
36
34
  *node_modules/*)
37
- if [ -d "$package_dir/dist/hooks" ]; then node "$bundle" "$@"; else node "$package_dir/dist/josh.js" "$josh_command" "$@"; fi
35
+ if [ -d "$package_dir/dist/hooks" ]; then exec node "$bundle" "$@"; else exec node "$package_dir/dist/josh.js" "$josh_command" "$@"; fi
38
36
  ;;
39
37
  *)
40
- node --disable-warning=ExperimentalWarning "$hooks_dir/hook-bundle-ready.ts" "$hook_name" "$josh_command" "$@"
38
+ exec node --disable-warning=ExperimentalWarning "$hooks_dir/hook-bundle-ready.ts" "$hook_name" "$josh_command" "$@"
41
39
  ;;
42
40
  esac
@@ -65,8 +65,9 @@ function notice(raw_payload: string, root: string = process.cwd()): string | und
65
65
 
66
66
  const target = STAMP.path(transcript)
67
67
 
68
- if (STAMP.last_ms(target) !== hook_decision.NEVER_MS) return undefined
69
- if (!STAMP.record(target, Date.now())) return undefined
68
+ if (STAMP.last_ms(target) !== hook_decision.NEVER_MS || !STAMP.record(target, Date.now())) {
69
+ return undefined
70
+ }
70
71
 
71
72
  return NOTICE
72
73
  }
@@ -0,0 +1,204 @@
1
+ import { git_gh_repo } from '#scripts/gh/git-gh-repo'
2
+ import { issue_cite } from '#scripts/issue/issue-cite'
3
+ import { issue_cite_cli } from '#scripts/issue/issue-cite-cli'
4
+ import { session_cite } from '#scripts/issue/session-cite'
5
+ import { bounded_pool } from '#scripts/lib/bounded-pool'
6
+ import { telegram_test } from '#scripts/notify/telegram-test'
7
+ import type { CliValues } from '#scripts/notify/telegram-test-logic'
8
+ import { issue_citation } from '#scripts/rules/issue-citation'
9
+ import { stop_rules, type StopAction, type StopOutcome } from '#scripts/rules/stop-rules'
10
+ import { run_hold } from '#scripts/run/hold/run-hold'
11
+ import { run_hold_cli } from '#scripts/run/hold/run-hold-cli'
12
+ import { run_hold_session } from '#scripts/run/hold/run-hold-session'
13
+
14
+ type NotifyAction = Extract<StopAction, { kind: 'notify' }>
15
+
16
+ // The `Stop` hook running the operations whose answer is fixed (joshuafolkken/kit#3919): the
17
+ // `confirmation` notify, the hold release and the citation lines, through the very calls
18
+ // `josh notify`, `josh run:release` and `josh issue:cite` make — in process, since three `pnpm`
19
+ // launches would spend most of the hook's 20-second budget. What each did reaches the person as a
20
+ // `systemMessage`; one that did not take falls back to the refusal it replaced (`stop_rules.fallback_reason`).
21
+
22
+ const SUCCESS_EXIT_CODE = 0
23
+ const NOTICE_SEPARATOR = '\n'
24
+ // The reply is the stop's reason, but a Telegram is read at a glance — its opening is enough.
25
+ const SUMMARY_CHARS = 600
26
+ // A `#N`, `owner/repo#N` or URL is a run of printable ASCII; a cut inside one would link the wrong Issue.
27
+ const TOKEN_CHAR = /^[!-~]$/u
28
+ const EMPTY_SUMMARY = 'The run stopped and is waiting for you.'
29
+
30
+ interface Settled {
31
+ notices: ReadonlyArray<string>
32
+ failure: string | undefined
33
+ }
34
+
35
+ // One action at a time — the order is the rule order.
36
+ const SEQUENTIAL = 1
37
+
38
+ // Where the run of token characters `text` ends with starts.
39
+ function token_start(text: string): number {
40
+ let index = text.length
41
+
42
+ while (index > 0 && TOKEN_CHAR.test(text.charAt(index - 1))) index -= 1
43
+
44
+ return index
45
+ }
46
+
47
+ // Cut before linking, on a boundary that keeps every reference whole: the character past the cut shows
48
+ // whether the last token ran on, and a token that did is dropped rather than halved.
49
+ function summary_of(message: string): string {
50
+ const trimmed = message.trim()
51
+ if (trimmed === '') return EMPTY_SUMMARY
52
+ if (trimmed.length <= SUMMARY_CHARS) return trimmed
53
+
54
+ const overshoot = trimmed.slice(0, SUMMARY_CHARS + 1)
55
+ const cut = overshoot.slice(0, Math.min(token_start(overshoot), SUMMARY_CHARS))
56
+
57
+ return cut === '' ? trimmed.slice(0, SUMMARY_CHARS) : cut
58
+ }
59
+
60
+ // The held Issue's URL, or `undefined` for a `new` entry or a repository that cannot be read.
61
+ async function issue_url_of(issue: string): Promise<string | undefined> {
62
+ if (issue === run_hold.UNNUMBERED_ISSUE) return undefined
63
+
64
+ const slug = await git_gh_repo.repo_get_name_with_owner()
65
+
66
+ return slug === undefined ? undefined : issue_cite.issue_url(slug, issue)
67
+ }
68
+
69
+ // The body is the reply's opening copied into a Telegram, so its bare `#N` are linked (`issue-citation.md`).
70
+ // The choices are the reply's own decision lines (joshuafolkken/kit#3961).
71
+ function notify_values(issue_url: string | undefined, action: NotifyAction): CliValues {
72
+ const { recommended, alternative } = action.decision
73
+ const values: CliValues = {
74
+ 'task-type': 'confirmation',
75
+ body: session_cite.text(summary_of(action.message)),
76
+ recommended,
77
+ ...(alternative !== undefined && { alternative: [alternative] }),
78
+ }
79
+
80
+ return issue_url === undefined ? values : { ...values, 'issue-url': issue_url }
81
+ }
82
+
83
+ // The send is stamped on the hold, so a later stop of the same pause does not announce it again. A
84
+ // stamp that fails does not fail the send: the Telegram is out, and a refused stop would send it twice.
85
+ async function mark_notified(): Promise<void> {
86
+ try {
87
+ const directory = await run_hold.worktree_directory()
88
+
89
+ if (directory !== undefined) run_hold_session.mark_notified(run_hold.hold_path(directory))
90
+ } catch (error) {
91
+ console.warn(`stop:guard could not stamp the notify on the hold: ${String(error)}`)
92
+ }
93
+ }
94
+
95
+ async function notify(action: NotifyAction): Promise<string> {
96
+ const issue_url = await issue_url_of(action.issue)
97
+
98
+ await telegram_test.send_values(notify_values(issue_url, action))
99
+ await mark_notified()
100
+
101
+ return `📱 stop:guard sent the confirmation notify (${issue_url ?? action.issue}).`
102
+ }
103
+
104
+ async function release(issue: string): Promise<string | undefined> {
105
+ const argv = issue === run_hold.UNNUMBERED_ISSUE ? ['--release'] : ['--release', issue]
106
+ const code = await run_hold_cli.run(argv)
107
+
108
+ return code === SUCCESS_EXIT_CODE ? '🔓 stop:guard released the run:hold record.' : undefined
109
+ }
110
+
111
+ // Every line or none: a citation with a number missing is the half-fix the refusal exists to avoid.
112
+ async function cite(references: ReadonlyArray<string>): Promise<string | undefined> {
113
+ const cited = await issue_cite_cli.cite(issue_citation.cite_arguments(references))
114
+ if (cited === undefined || cited.failures.length > 0) return undefined
115
+
116
+ return ['Issue citations:', ...cited.lines].join(NOTICE_SEPARATOR)
117
+ }
118
+
119
+ // An action's line for the person, or `undefined` when it did not take.
120
+ async function run_action(action: StopAction): Promise<string | undefined> {
121
+ if (action.kind === 'notify') return await notify(action)
122
+ if (action.kind === 'release') return await release(action.issue)
123
+
124
+ return await cite(action.references)
125
+ }
126
+
127
+ // **The hook's stdout is the decision envelope, so nothing else may reach it**: the reused calls report
128
+ // on stdout (`josh run:release`'s verdict token, the notify's sent line) and are sent to stderr instead.
129
+ type StreamWrite = typeof process.stdout.write
130
+
131
+ function set_stdout_write(stream_write: StreamWrite): void {
132
+ process.stdout.write = stream_write
133
+ }
134
+
135
+ async function on_stderr<T>(work: () => Promise<T>): Promise<T> {
136
+ const saved = { write: process.stdout.write.bind(process.stdout) }
137
+
138
+ set_stdout_write(process.stderr.write.bind(process.stderr))
139
+
140
+ try {
141
+ return await work()
142
+ } finally {
143
+ set_stdout_write(saved.write)
144
+ }
145
+ }
146
+
147
+ // A throw is an action that did not take, the same as one that answered it had failed.
148
+ async function attempt(action: StopAction): Promise<string | undefined> {
149
+ try {
150
+ return await run_action(action)
151
+ } catch {
152
+ return undefined
153
+ }
154
+ }
155
+
156
+ function is_notice(notice: string | undefined): notice is string {
157
+ return notice !== undefined
158
+ }
159
+
160
+ // In order, one at a time: the notify reports the hold the release then ends. The first failure is the
161
+ // one refused with, as the rule order put it first.
162
+ async function settle(actions: ReadonlyArray<StopAction>): Promise<Settled> {
163
+ const notices = await on_stderr(
164
+ async () => await bounded_pool.bounded_map(actions, SEQUENTIAL, attempt),
165
+ )
166
+ const failed = actions.find((_action, index) => notices[index] === undefined)
167
+
168
+ return {
169
+ notices: notices.filter((notice) => is_notice(notice)),
170
+ failure: failed === undefined ? undefined : stop_rules.fallback_reason(failed),
171
+ }
172
+ }
173
+
174
+ function joined(notices: ReadonlyArray<string>): string | undefined {
175
+ return notices.length === 0 ? undefined : notices.join(NOTICE_SEPARATOR)
176
+ }
177
+
178
+ // A failure refuses only where the outcome allows it — never past the loop-breaker.
179
+ function settled_envelope(
180
+ settled: Settled,
181
+ is_blocking: boolean,
182
+ lang: string,
183
+ ): string | undefined {
184
+ const notice = joined(settled.notices)
185
+
186
+ if (is_blocking && settled.failure !== undefined) {
187
+ return stop_rules.block_envelope(settled.failure, lang, notice)
188
+ }
189
+
190
+ return notice === undefined ? undefined : stop_rules.notice_envelope(notice)
191
+ }
192
+
193
+ // What the hook writes for one stop's outcome, or `undefined` for a stop that writes nothing.
194
+ async function envelope_of(outcome: StopOutcome, lang: string): Promise<string | undefined> {
195
+ if (outcome.reason !== undefined) return stop_rules.block_envelope(outcome.reason, lang)
196
+
197
+ const settled = await settle(outcome.actions)
198
+
199
+ return settled_envelope(settled, outcome.blocks_on_failure, lang)
200
+ }
201
+
202
+ const stop_actions = { envelope_of, summary_of }
203
+
204
+ export { stop_actions }