@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
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env tsx
2
- import { availableParallelism } from 'node:os'
3
2
  import { fileURLToPath } from 'node:url'
4
3
  import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
5
4
  import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
6
5
  import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
7
6
  import { lane_ledger } from '#scripts/lane/lane-ledger'
8
- import { bounded_pool } from '#scripts/lib/bounded-pool'
9
- import { buffered_process, FAIL_EXIT_CODE } from '#scripts/lib/buffered-process'
7
+ import { FAIL_EXIT_CODE } from '#scripts/lib/buffered-process'
10
8
  import { review_stamps } from '#scripts/review/review-stamps'
11
9
  import { review_tree } from '#scripts/review/review-tree'
12
10
  import { unit_worker_share } from '#scripts/test/unit-worker-share'
13
- import { core_budget } from './core-budget'
11
+ import { check_reuse, type CheckRecords } from './check-reuse'
12
+ import { free_cores } from './free-cores'
14
13
  import { gate_ledger, type GateLedgerStart } from './gate-ledger'
15
14
  import { gate_plan, type GatePlan } from './gate-plan'
16
- import { gate_report, type GateStep, type GateStepResult } from './gate-report'
15
+ import { gate_report, type GateStepResult } from './gate-report'
16
+ import { gate_run_steps, type ReusedChecks } from './gate-run-steps'
17
17
  import { gate_skip } from './gate-skip'
18
18
  import { build_gate_step, build_gate_steps, UNIT_WORKER_FLAG } from './gate-steps'
19
19
  import { gate_tree, type GateTree } from './gate-tree'
@@ -35,25 +35,6 @@ const { GATE_CHECKS } = gate_plan
35
35
 
36
36
  const GATE_TARGETS: ReadonlyArray<string> = GATE_CHECKS.map((check) => check.target)
37
37
 
38
- async function run_gate_step(step: GateStep): Promise<GateStepResult> {
39
- if (step.skip_reason !== undefined) {
40
- return {
41
- label: step.label,
42
- command: step.command_args.join(' '),
43
- output: project_checks.skip_notice('check', step.skip_reason),
44
- exit_code: 0,
45
- elapsed_ms: 0,
46
- }
47
- }
48
-
49
- const result = await buffered_process.run_buffered_process(
50
- step.command_args,
51
- step.cwd === undefined ? {} : { cwd: step.cwd },
52
- )
53
-
54
- return { label: step.label, command: step.command_args.join(' '), ...result }
55
- }
56
-
57
38
  // The record `josh review:brief` reads, written only on a fully green run and only with the tree it
58
39
  // was green on. `/code-review` runs in a forked process that reads none of
59
40
  // this repository's documents, so "the unit tests already passed" reaches it only if the invocation
@@ -65,7 +46,7 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
65
46
  // visible on the console, and a record erasing it would have the brief tell a review agent not to
66
47
  // re-run tests that never ran. A skip of a tool the project does not use (`NOT_USED_MARKER` — eslint
67
48
  // with no ESLint configuration) is not one: nothing went unverified, so it withholds nothing
68
- // (`gate_report.is_unverified_skip`). A step that passed **with a checker's warnings** is withheld for the
49
+ // (`gate_report.has_nothing_to_say`). A step that passed **with a checker's warnings** is withheld for the
69
50
  // same reason one place further on: since the record is reused instead of the checks being re-run, a
70
51
  // warning printed once would never be printed again on that tree, and hiding an eslint or
71
52
  // svelte-check finding is the same failure as hiding a skip. It is `has_checker_warning`, not
@@ -81,17 +62,13 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
81
62
  //
82
63
  // The destination is a parameter so a test can exercise the record without overwriting the one a
83
64
  // real run may be relying on — `josh gate` and `josh review:brief` share one path by design.
84
- function has_nothing_to_say(result: GateStepResult): boolean {
85
- return !gate_report.is_unverified_skip(result) && !gate_report.has_checker_warning(result)
86
- }
87
-
88
65
  async function record_green_gate(
89
66
  results: ReadonlyArray<GateStepResult>,
90
67
  before: Record<string, string>,
91
68
  target?: string,
92
69
  base?: string,
93
70
  ): Promise<void> {
94
- if (results.some((result) => !has_nothing_to_say(result))) return
71
+ if (results.some((result) => !gate_report.has_nothing_to_say(result))) return
95
72
 
96
73
  try {
97
74
  const after = await review_tree.read_changed_tree()
@@ -153,94 +130,6 @@ async function with_gate_marker<T>(
153
130
  }
154
131
  }
155
132
 
156
- // This gate's share of the machine, resolved once at the gate's entry and carried down as one value.
157
- // `available_cores` is the budget every check reserves against; `is_reserved`
158
- // is false for a gate nested inside another gate's unit suite — this repository's own gate tests — so
159
- // the outer gate reserves and every gate beneath it does not. Captured before this gate sets
160
- // `JOSH_UNIT_RUN_MARKED`, which is why it is read at entry rather than re-derived here.
161
- interface BudgetContext {
162
- available_cores: number
163
- is_reserved: boolean
164
- }
165
-
166
- function default_budget(): BudgetContext {
167
- return {
168
- available_cores: availableParallelism(),
169
- is_reserved: !unit_worker_share.is_nested_run() && !core_budget.is_held(),
170
- }
171
- }
172
-
173
- // A gate step paired with the cores it reserves from the machine-wide budget while it runs.
174
- // The weight travels with the step so `run_gate_steps` never has to re-pair a
175
- // step with its check by index.
176
- interface ReservedStep {
177
- step: GateStep
178
- weight: number
179
- memory_mb: number
180
- }
181
-
182
- async function build_reserved_steps(
183
- start_directory: string,
184
- plan: GatePlan,
185
- available_cores: number,
186
- ): Promise<ReadonlyArray<ReservedStep>> {
187
- return await Promise.all(
188
- plan.checks.map(async (check) => ({
189
- step: await build_gate_step(check, start_directory, plan),
190
- weight: gate_plan.check_weight(check, plan, available_cores),
191
- memory_mb: gate_plan.check_memory(check, plan, available_cores),
192
- })),
193
- )
194
- }
195
-
196
- // **A nested gate takes no reservation, and the budget is what makes that necessary rather than
197
- // merely tidy**. The unit step spawns the suite, which runs this repository's
198
- // own gate tests; each of those is a gate inside the outer gate's unit reservation, and a place it
199
- // claimed would wait for cores the outer gate cannot free until the suite — this test included —
200
- // finishes.
201
- async function run_reserved_step(
202
- reserved: ReservedStep,
203
- budget: BudgetContext,
204
- ): Promise<GateStepResult> {
205
- if (!budget.is_reserved) return await run_gate_step(reserved.step)
206
-
207
- return await core_budget.with_core_reservation(
208
- reserved.weight,
209
- async (admission) => ({
210
- ...(await run_gate_step(reserved.step)),
211
- budget_note: core_budget.describe_overflow(admission),
212
- }),
213
- { budget: budget.available_cores, memory_mb: reserved.memory_mb },
214
- )
215
- }
216
-
217
- // `bounded_pool` rather than a bare `Promise.all`, so the plan's `concurrency` is what decides how
218
- // many run at once. **The all-failures-in-one-pass property survives the
219
- // change** because no check ever rejects: `buffered_process` reports a non-zero exit as a value, so
220
- // the pool's first-failure abort — written for callers that spawn real Claude sessions — never
221
- // fires here and every queued check still runs. Results come back in input order however they
222
- // finished, which is what keeps the printed sections in declaration order. Each step first claims its
223
- // place in the machine-wide budget and waits while the budget is full.
224
- async function run_gate_steps(
225
- plan: GatePlan,
226
- budget: BudgetContext = default_budget(),
227
- ): Promise<ReadonlyArray<GateStepResult>> {
228
- const reserved = await build_reserved_steps(process.cwd(), plan, budget.available_cores)
229
-
230
- // **The checks run under the held mark, so a check's own dispatch claims nothing**.
231
- // Each check is a `pnpm josh <target>` child, and a target that declares
232
- // a weight would otherwise claim a second place for the cores reserved for it here. A nested gate
233
- // sets the mark too, since its checks are covered by the outer gate's reservation.
234
- return await core_budget.with_held_mark(
235
- async () =>
236
- await bounded_pool.bounded_map(
237
- reserved,
238
- plan.concurrency,
239
- async (entry) => await run_reserved_step(entry, budget),
240
- ),
241
- )
242
- }
243
-
244
133
  // **Both markers are claims about the unit suite, so a gate that is not running it makes neither**.
245
134
  // The in-flight marker tells the next `josh review:brief` that a gate is
246
135
  // covering this tree right now, which is what stops a review agent re-running the suite; the
@@ -276,15 +165,21 @@ async function run_marked_gate<T>(
276
165
  // The concurrent-run count is read here rather than inside `gate-plan.ts` for the reason the core
277
166
  // count already is: that module stays a pure function of its inputs, and the machine is asked once and
278
167
  // handed to both calls. Counted *before* the unit step writes its own marker, so `+ 1` is this gate.
279
- function announce_gate_plan(is_unit_included: boolean, available_cores: number): GatePlan {
168
+ // `free` bounds the unit cap the run count produced (`gate_plan.bound_by_free`).
169
+ function announce_gate_plan(
170
+ is_unit_included: boolean,
171
+ available_cores: number,
172
+ free: number | undefined,
173
+ ): GatePlan {
280
174
  const concurrent_runs = unit_worker_share.live_run_count() + unit_worker_share.SOLO_RUNS
281
175
  const is_kit_repository = project_checks.is_kit_repository(process.cwd())
282
- const plan = gate_plan.resolve_gate_plan(
176
+ const resolved = gate_plan.resolve_gate_plan(
283
177
  available_cores,
284
178
  concurrent_runs,
285
179
  is_unit_included,
286
180
  is_kit_repository,
287
181
  )
182
+ const plan = gate_plan.bound_by_free(resolved, available_cores, free)
288
183
 
289
184
  process.stdout.write(`${gate_plan.format_gate_plan(plan, available_cores, concurrent_runs)}\n`)
290
185
 
@@ -319,6 +214,35 @@ interface GateOptions {
319
214
  // The lane ledger a finished gate's duration is appended to. Only the CLI
320
215
  // entry resolves it, so a suite driving the gate never writes a measurement of checks that never ran.
321
216
  ledger_path?: string | undefined
217
+ // Where each check's own green record lives (`check-reuse.ts`). Only the CLI entry resolves it, for
218
+ // the ledger's reason: a suite whose checks never ran must not record them green on the live tree.
219
+ check_records?: CheckRecords | undefined
220
+ // The cores free of load from outside the core budget's ledger (`free-cores.ts`). Only the CLI entry
221
+ // resolves it, for the ledger's reason: a suite driving the gate never reads the machine, so its plan
222
+ // does not depend on what else happened to be running.
223
+ read_free_cores?: ((available_cores: number) => Promise<number | undefined>) | undefined
224
+ }
225
+
226
+ // **Per-check records belong to the full local gate alone**: CI's `--no-unit` gate is the safety net
227
+ // under the local reuse and stays as it was, so it neither reads nor writes one.
228
+ function check_records_of(plan: GatePlan, options: GateOptions): CheckRecords | undefined {
229
+ return gate_plan.has_unit_check(plan.checks) ? options.check_records : undefined
230
+ }
231
+
232
+ async function checks_to_reuse(
233
+ plan: GatePlan,
234
+ tree: GateTree,
235
+ options: GateOptions,
236
+ ): Promise<ReusedChecks> {
237
+ const records = check_records_of(plan, options)
238
+
239
+ if (records === undefined || options.is_forced === true) return new Map()
240
+
241
+ return await check_reuse.reusable_checks(
242
+ plan.checks.map((check) => check.label),
243
+ tree,
244
+ records,
245
+ )
322
246
  }
323
247
 
324
248
  // **The gate refuses to start when the scoped pair has not been green on this tree**
@@ -328,8 +252,13 @@ interface GateOptions {
328
252
  // `--force` is the caller saying to run regardless. `scoped_green` owns the record and the `JOSH_SCOPED_GREEN`
329
253
  // escape hatch, so a person can always get past a record that is wrong about their tree.
330
254
  function scoped_precheck_refusal(tree: GateTree, options: GateOptions): string | undefined {
331
- if (options.is_scoped_enforced !== true) return undefined
332
- if (options.is_forced === true || options.is_unit_included === false) return undefined
255
+ if (
256
+ options.is_scoped_enforced !== true ||
257
+ options.is_forced === true ||
258
+ options.is_unit_included === false
259
+ ) {
260
+ return undefined
261
+ }
333
262
 
334
263
  return scoped_green.refusal_for(tree.files, tree.base, options.scoped_sources)
335
264
  }
@@ -369,6 +298,10 @@ async function settle_gate(
369
298
 
370
299
  await gate_ledger.record(options.ledger, results, { elapsed_ms, is_passed })
371
300
 
301
+ const records = check_records_of(plan, options)
302
+
303
+ if (records !== undefined) await check_reuse.record_green_checks(results, tree, records)
304
+
372
305
  if (!is_passed) return FAIL_EXIT_CODE
373
306
 
374
307
  await record_whole_gate(plan, results, tree, options)
@@ -391,8 +324,11 @@ async function run_checked_gate(
391
324
  const ledger = await gate_ledger.start(options.ledger_path)
392
325
  // Resolved at the gate's entry, before this gate sets `JOSH_UNIT_RUN_MARKED`, so a gate nested in the
393
326
  // unit suite reads the flag as set and reserves nothing.
394
- const budget = default_budget()
395
- const plan = announce_gate_plan(is_unit_included, budget.available_cores)
327
+ const budget = gate_run_steps.default_budget()
328
+ // Read before the count for the ledger's reason: the reading waits out a sample window.
329
+ const free = await options.read_free_cores?.(budget.available_cores)
330
+ const plan = announce_gate_plan(is_unit_included, budget.available_cores, free)
331
+ const reused = await checks_to_reuse(plan, tree, options)
396
332
 
397
333
  // **The gate holds the unit-run marker for its whole run, not just its unit step.** The step that
398
334
  // would write it is a subprocess started a second or so after the count above, so two lanes launched
@@ -401,7 +337,7 @@ async function run_checked_gate(
401
337
  // the next lane asks; the guard inside the spawned `josh test:unit` sees the handoff and adds no
402
338
  // second marker for the same run.
403
339
  async function run_and_settle(): Promise<number> {
404
- const results = await run_gate_steps(plan, budget)
340
+ const results = await gate_run_steps.run_gate_steps(plan, budget, reused)
405
341
 
406
342
  return await settle_gate(plan, results, tree, { ...options, started_at, ledger })
407
343
  }
@@ -501,6 +437,8 @@ async function run_gate_command(
501
437
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
502
438
  process.exitCode = await run_gate_command(process.argv.slice(FIRST_ARGUMENT_INDEX), {
503
439
  ledger_path: await lane_ledger.target(),
440
+ check_records: check_reuse.DEFAULT_RECORDS,
441
+ read_free_cores: async (available_cores) => await free_cores.read({ available_cores }),
504
442
  })
505
443
  }
506
444
 
@@ -514,11 +452,11 @@ const verification_gate = {
514
452
  clear_gate_running,
515
453
  mark_gate_running,
516
454
  record_green_gate,
517
- run_gate_steps,
455
+ run_gate_steps: gate_run_steps.run_gate_steps,
518
456
  scoped_precheck_refusal,
519
457
  with_gate_marker,
520
458
  run_gate_command,
521
- run_gate_step,
459
+ run_gate_step: gate_run_steps.run_gate_step,
522
460
  run_verification_gate,
523
461
  }
524
462
 
@@ -71,6 +71,7 @@ const REST_FIELD_NAMES = new Map<string, string>([
71
71
  ['url', 'html_url'],
72
72
  ['createdAt', 'created_at'],
73
73
  ['closedAt', 'closed_at'],
74
+ ['stateReason', 'state_reason'],
74
75
  ['updatedAt', 'updated_at'],
75
76
  ])
76
77
 
@@ -36,6 +36,7 @@ const LABEL_NAMES_FILTER = '.[].name'
36
36
  const LABELS_SEGMENT = '/labels'
37
37
  const COMMENTS_SEGMENT = '/comments'
38
38
  const CLOSED_STATE = 'closed'
39
+ const NOT_PLANNED_REASON = 'not_planned'
39
40
  const COLOR_HASH = '#'
40
41
  const UNPROCESSABLE_STATUS = 422
41
42
  const ALREADY_EXISTS_CODE = 'already_exists'
@@ -99,17 +100,37 @@ async function issue_try_comment(issue_number: string, body: string): Promise<bo
99
100
  // It is a required parameter that may be `undefined` rather than an optional one: skipping the
100
101
  // announcement is a decision the caller makes from what it read, never something a call site can
101
102
  // fall into by leaving an argument off.
102
- async function issue_close(issue_number: string, comment: string | undefined): Promise<boolean> {
103
+ async function close_with(
104
+ issue_number: string,
105
+ comment: string | undefined,
106
+ state: Readonly<Record<string, string>>,
107
+ ): Promise<boolean> {
103
108
  return await did_write_succeed(async () => {
104
109
  if (comment !== undefined) await issue_comment(issue_number, comment)
105
110
  await git_gh_exec.exec_gh_api({
106
111
  path: git_gh_api_path.issue_api_path(issue_number),
107
112
  method: PATCH_METHOD,
108
- body: JSON.stringify({ state: CLOSED_STATE }),
113
+ body: JSON.stringify(state),
109
114
  })
110
115
  })
111
116
  }
112
117
 
118
+ async function issue_close(issue_number: string, comment: string | undefined): Promise<boolean> {
119
+ return await close_with(issue_number, comment, { state: CLOSED_STATE })
120
+ }
121
+
122
+ // The same close as "not planned" — the reason `issue-merged.ts` reads as no merge, so a closed issue
123
+ // that was withdrawn is never taken for one whose work landed.
124
+ async function issue_close_not_planned(
125
+ issue_number: string,
126
+ comment: string | undefined,
127
+ ): Promise<boolean> {
128
+ return await close_with(issue_number, comment, {
129
+ state: CLOSED_STATE,
130
+ state_reason: NOT_PLANNED_REASON,
131
+ })
132
+ }
133
+
113
134
  // REST answers 422 `{"resource":"Label","code":"invalid","field":"color"}` for a color carrying a
114
135
  // leading `#`. The one caller passes the hash form (`EPIC_LABEL_COLOR`), so dropping it here is what
115
136
  // keeps that call site unchanged.
@@ -336,6 +357,7 @@ const git_gh_issue_write = {
336
357
  issue_comment,
337
358
  issue_try_comment,
338
359
  issue_close,
360
+ issue_close_not_planned,
339
361
  label_ensure,
340
362
  issue_create_request,
341
363
  issue_create_with_label,
@@ -5,6 +5,7 @@ import { issue_cite } from '#scripts/issue/issue-cite'
5
5
  import { session_cite } from '#scripts/issue/session-cite'
6
6
  import { animation_helpers, type AnimationOptions } from '#scripts/lib/animation-helpers'
7
7
  import { json_value } from '#scripts/lib/json-value'
8
+ import { acceptance_map } from '#scripts/review/acceptance-map'
8
9
  import { git_gh_command } from './git-gh-command'
9
10
  import { git_pr_error } from './git-pr-error'
10
11
  import { git_pr_messages } from './git-pr-messages'
@@ -172,9 +173,24 @@ function build_body(issue_info: IssueInfo, extra_body?: string): string {
172
173
  return `${closes}\n\n${extra_body}`
173
174
  }
174
175
 
176
+ // A body without the acceptance-criteria table gets one pending row per criterion, so the author fills
177
+ // in rows that already carry their identifiers — `josh followup` refuses the merge until they do.
178
+ async function body_with_map(issue_info: IssueInfo, extra_body?: string): Promise<string> {
179
+ const body = build_body(issue_info, extra_body)
180
+
181
+ if (acceptance_map.has_map(body)) return body
182
+ const map = acceptance_map.skeleton(await git_gh_command.issue_get_body(issue_info.number))
183
+
184
+ return map === undefined ? body : `${body}\n\n${map}`
185
+ }
186
+
175
187
  async function existing_pr_label(branch_name: string): Promise<ReleaseClassification | undefined> {
176
- if (!(await git_gh_command.pr_exists(branch_name))) return undefined
177
- if (is_pr_state_merged(await get_pr_state(branch_name))) return undefined
188
+ if (
189
+ !(await git_gh_command.pr_exists(branch_name)) ||
190
+ is_pr_state_merged(await get_pr_state(branch_name))
191
+ ) {
192
+ return undefined
193
+ }
178
194
 
179
195
  return await git_gh_command.pr_get_classification(branch_name)
180
196
  }
@@ -186,7 +202,7 @@ async function create_for_issue(
186
202
  ): Promise<void> {
187
203
  await create(
188
204
  build_title(issue_info),
189
- build_body(issue_info, extra_body),
205
+ await body_with_map(issue_info, extra_body),
190
206
  issue_info.branch_name,
191
207
  {
192
208
  label,
@@ -88,9 +88,9 @@ function select_linked(found: ReadonlyArray<string>): string | undefined {
88
88
  const own_directory = found[OWN_GIT_DIRECTORY]
89
89
  const common = found[COMMON_GIT_DIRECTORY]
90
90
 
91
- return own_directory !== undefined && common !== undefined && own_directory !== common
92
- ? common
93
- : undefined
91
+ return own_directory === undefined || common === undefined || own_directory === common
92
+ ? undefined
93
+ : common
94
94
  }
95
95
 
96
96
  function resolve(cwd: string): string | undefined {
@@ -9,19 +9,24 @@ const REFS_REMOTES_ORIGIN_PREFIX = 'refs/remotes/origin/'
9
9
  const NAME_ONLY_FLAG = '--name-only'
10
10
  const DEFAULT_BRANCH_FALLBACK = 'main'
11
11
 
12
+ const DEFAULT_BRANCH_ARGUMENTS = ['symbolic-ref', 'refs/remotes/origin/HEAD'] as const
13
+
14
+ // The default branch `DEFAULT_BRANCH_ARGUMENTS` names, or the fallback for a missing or unexpected
15
+ // answer — shared with the synchronous read the implementation-cut guard takes it with.
16
+ function default_branch_of(output: string | undefined): string {
17
+ const trimmed = output?.trim() ?? ''
18
+
19
+ if (!trimmed.startsWith(REFS_REMOTES_ORIGIN_PREFIX)) return DEFAULT_BRANCH_FALLBACK
20
+
21
+ return trimmed.slice(REFS_REMOTES_ORIGIN_PREFIX.length)
22
+ }
23
+
12
24
  async function get_default_branch(): Promise<string> {
13
25
  try {
14
- const output = await git_spawn.read(['symbolic-ref', 'refs/remotes/origin/HEAD'])
15
- const trimmed = output.trim()
16
-
17
- if (trimmed.startsWith(REFS_REMOTES_ORIGIN_PREFIX)) {
18
- return trimmed.slice(REFS_REMOTES_ORIGIN_PREFIX.length)
19
- }
26
+ return default_branch_of(await git_spawn.read([...DEFAULT_BRANCH_ARGUMENTS]))
20
27
  } catch {
21
- // fall through to default
28
+ return DEFAULT_BRANCH_FALLBACK
22
29
  }
23
-
24
- return DEFAULT_BRANCH_FALLBACK
25
30
  }
26
31
 
27
32
  const MERGE_BASE_COMMAND = 'merge-base'
@@ -210,8 +215,10 @@ async function untracked_names(): Promise<string> {
210
215
  }
211
216
 
212
217
  const git_diff_reads = {
218
+ DEFAULT_BRANCH_ARGUMENTS,
213
219
  change_base,
214
220
  change_base_commit,
221
+ default_branch_of,
215
222
  default_branch_reference,
216
223
  diff_cached_names,
217
224
  diff_main,
@@ -1,8 +1,7 @@
1
1
  // Extract the exit code from a thrown execa error. Returns the numeric exit code,
2
2
  // or undefined when the process was terminated by a signal or failed to spawn.
3
3
  function get_exit_code(error: unknown): number | undefined {
4
- if (typeof error !== 'object' || error === null) return undefined
5
- if (!('exitCode' in error)) return undefined
4
+ if (typeof error !== 'object' || error === null || !('exitCode' in error)) return undefined
6
5
 
7
6
  const { exitCode: exit_code } = error
8
7
 
@@ -0,0 +1,78 @@
1
+ import { SUITE_TIMEOUT_MS } from '#scripts/lib/timeouts'
2
+ import { execa } from 'execa'
3
+ import { git_command } from './git-command'
4
+ import { git_spawn } from './git-spawn'
5
+
6
+ // A merge of the default branch that stops only on generated files is finished here rather than by a
7
+ // resumed session: each such file is the output of a command, so running that command on the merged
8
+ // tree writes the one correct resolution, with no conflict marker for anyone to read.
9
+ //
10
+ // **This table is the single list of what is resolved that way** — a file a person writes never
11
+ // belongs in it, because regenerating would discard that person's side of the merge. A conflict on any
12
+ // path not listed here still stops as `conflict`. A file belongs here only once its committed content
13
+ // is exactly what its command writes: `scripts/test/pure-files.ts` is not, so regenerating it would
14
+ // rewrite far more than the conflict.
15
+
16
+ interface RegeneratedFile {
17
+ path: string
18
+ command: ReadonlyArray<string>
19
+ }
20
+
21
+ const REGENERATED_FILES: ReadonlyArray<RegeneratedFile> = [
22
+ {
23
+ path: 'docs/josh-command-catalog.md',
24
+ command: ['pnpm', 'exec', 'tsx', 'scripts/document/generate-catalog.ts'],
25
+ },
26
+ ]
27
+
28
+ function regenerated_for(
29
+ files: ReadonlyArray<string>,
30
+ table: ReadonlyArray<RegeneratedFile>,
31
+ ): ReadonlyArray<RegeneratedFile> {
32
+ const entries = files.map((file) => table.find((entry) => entry.path === file))
33
+
34
+ return entries.every((entry) => entry !== undefined) ? entries : []
35
+ }
36
+
37
+ // Each generator writes only its own file, so they run side by side.
38
+ async function regenerate(entries: ReadonlyArray<RegeneratedFile>, cwd: string): Promise<void> {
39
+ await Promise.all(
40
+ entries.map(async (entry) => {
41
+ const [binary = '', ...command_arguments] = entry.command
42
+
43
+ await execa(binary, command_arguments, { cwd })
44
+ }),
45
+ )
46
+ }
47
+
48
+ // `--no-edit` keeps the message the merge was started with — the one carrying the issue number the
49
+ // `commit-msg` hook asks for.
50
+ async function conclude_merge(entries: ReadonlyArray<RegeneratedFile>): Promise<void> {
51
+ await git_spawn.with_output('add', ['--', ...entries.map((entry) => entry.path)])
52
+ await git_spawn.with_output('commit', ['--no-edit'], { timeout_ms: SUITE_TIMEOUT_MS })
53
+ }
54
+
55
+ // Whether the merge that stopped on `files` was finished. A command that fails leaves the merge in
56
+ // progress and answers `false`, so the stop is the `conflict` it would have been without this table.
57
+ async function resolve(
58
+ files: ReadonlyArray<string>,
59
+ table: ReadonlyArray<RegeneratedFile> = REGENERATED_FILES,
60
+ ): Promise<boolean> {
61
+ const entries = regenerated_for(files, table)
62
+
63
+ if (entries.length === 0) return false
64
+
65
+ try {
66
+ await regenerate(entries, await git_command.repository_root())
67
+ await conclude_merge(entries)
68
+ } catch {
69
+ return false
70
+ }
71
+
72
+ return true
73
+ }
74
+
75
+ const main_merge_regenerate = { REGENERATED_FILES, resolve }
76
+
77
+ export type { RegeneratedFile }
78
+ export { main_merge_regenerate }
@@ -6,6 +6,7 @@ import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-c
6
6
  import { error_text } from '#scripts/lib/error-message'
7
7
  import { git_command } from './git-command'
8
8
  import { main_merge_guard } from './main-merge-guard'
9
+ import { main_merge_regenerate } from './main-merge-regenerate'
9
10
 
10
11
  // `josh main:merge` (`josh mm`) — bring the default branch into the branch this checkout is on.
11
12
  //
@@ -62,18 +63,26 @@ interface MergeTarget {
62
63
  }
63
64
 
64
65
  // A failure that left no unmerged path — a remote that cannot be reached, a hook that refused — is not
65
- // a conflict, so it is rethrown rather than reported as one with nothing to resolve.
66
+ // a conflict, so it is rethrown rather than reported as one with nothing to resolve. A conflict only on
67
+ // generated files is regenerated and committed (`main-merge-regenerate.ts`), so it answers `undefined`
68
+ // and the merge reads as finished.
69
+ async function unresolved_conflict(error: unknown): Promise<MergeOutcome | undefined> {
70
+ const files = main_merge_guard.unmerged_paths(await git_command.status())
71
+
72
+ if (files.length === 0) throw error
73
+
74
+ return (await main_merge_regenerate.resolve(files)) ? undefined : { kind: 'conflict', files }
75
+ }
76
+
66
77
  async function merge_into(target: MergeTarget): Promise<MergeOutcome> {
67
78
  const { default_branch, current_branch } = target
68
79
 
69
80
  try {
70
81
  await git_command.merge_branch(default_branch, merge_message(default_branch, current_branch))
71
82
  } catch (error) {
72
- const files = main_merge_guard.unmerged_paths(await git_command.status())
73
-
74
- if (files.length === 0) throw error
83
+ const conflict = await unresolved_conflict(error)
75
84
 
76
- return { kind: 'conflict', files }
85
+ if (conflict !== undefined) return conflict
77
86
  }
78
87
 
79
88
  return { kind: target.is_current ? 'current' : 'merged', branch: default_branch }
@@ -54,16 +54,14 @@ function batching_mode(): LaneGuardMode {
54
54
  return lane_guard_policy.mode_here('batching')
55
55
  }
56
56
 
57
- // A guard the enumeration turns fully `off` says nothing at all. Batching is `notice` in a lane child
58
- // since kit#2276, so a candidate call is reached there; only a guard some future enumeration sets to
59
- // `off` is skipped. On the main line the mode is `refuse`.
57
+ // A guard the enumeration turns fully `off` says nothing at all. The lane-child mode and its reason
58
+ // live in `lane-guard-policy.ts`; on the main line the mode is `refuse`.
60
59
  function is_candidate(call: GuardedCall): boolean {
61
60
  return is_batch_candidate(call) && batching_mode() !== 'off'
62
61
  }
63
62
 
64
- // A refusable call is refused only where the mode is `refuse`. A lane child is `notice` (kit#2276), so
65
- // it reaches here and is withheld the refusal — the notice branch below carries the guidance instead,
66
- // because a refusal ends a headless child's turn (kit#2138).
63
+ // A refusable call is refused only where the mode is `refuse`. Under any other mode it is withheld the
64
+ // refusal; why a lane child takes that mode is its `because` in `lane-guard-policy.ts`.
67
65
  function should_block_here(tail: string, call: GuardedCall, refused_at_ms: number): boolean {
68
66
  if (batching_mode() !== 'refuse') return false
69
67
 
@@ -71,8 +69,7 @@ function should_block_here(tail: string, call: GuardedCall, refused_at_ms: numbe
71
69
  }
72
70
 
73
71
  // The whole-file write earns a notice on every run. The second branch turns a
74
- // refusable call into a notice under a `notice`-mode guard, which is the mode batching takes in a lane
75
- // child again since kit#2276. Both read the notice's own record, so a notice never spends the refusal's
72
+ // refusable call into a notice under a `notice`-mode guard. Both read the notice's own record, so a notice never spends the refusal's
76
73
  // stamp and re-fires on the notice's own `NOTICE_REFIRE_EVERY` cadence.
77
74
  function should_notify_here(tail: string, call: GuardedCall, notified_at_ms: number): boolean {
78
75
  if (time_batch_guard.should_notify(tail, call, notified_at_ms)) return true
@@ -251,8 +251,13 @@ function plan_daemon_restart(
251
251
  ): ReadonlyArray<FormatCommand> {
252
252
  const relative = relative_to_root(project_root, file_path)
253
253
 
254
- if (relative === undefined || !is_eslint_config_input(relative)) return []
255
- if (resolve_daemon_cli(ESLINT_DAEMON, project_root) === undefined) return []
254
+ if (
255
+ relative === undefined ||
256
+ !is_eslint_config_input(relative) ||
257
+ resolve_daemon_cli(ESLINT_DAEMON, project_root) === undefined
258
+ ) {
259
+ return []
260
+ }
256
261
 
257
262
  return [{ bin: ESLINT_DAEMON, command_arguments: [DAEMON_RESTART] }]
258
263
  }
@@ -325,8 +330,9 @@ async function run_command(command: FormatCommand, project_root: string): Promis
325
330
  // something to say. A start failure (non-zero, empty) has already been retried and carries no
326
331
  // problems, and prettier's stdout is the list of files it wrote rather than anything to act on.
327
332
  function eslint_unfixed(command: FormatCommand, outcome: CommandOutcome): string | undefined {
328
- if (command.bin !== ESLINT_BIN) return undefined
329
- if (outcome.exit_code === 0 || outcome.stdout.length === 0) return undefined
333
+ if (command.bin !== ESLINT_BIN || outcome.exit_code === 0 || outcome.stdout.length === 0) {
334
+ return undefined
335
+ }
330
336
 
331
337
  return outcome.stdout
332
338
  }