@joshuafolkken/kit 1.1178.0 → 1.1229.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 (425) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/settings.json +2 -2
  3. package/.claude/skills/dependency-update/SKILL.md +4 -8
  4. package/.claude/skills/epic-commands/SKILL.md +2 -2
  5. package/.claude/skills/epic-commands/epic-bundle.md +1 -1
  6. package/.claude/skills/workflow-commands/SKILL.md +2 -4
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -2
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +23 -2
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -2
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +4 -3
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +21 -13
  13. package/.claude/skills/workflow-commands/chain-rule.md +1 -1
  14. package/.claude/skills/workflow-commands/delegation.md +2 -2
  15. package/.claude/skills/workflow-commands/followup.md +5 -0
  16. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  17. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  18. package/.claude/skills/workflow-commands/issue-scout.md +1 -1
  19. package/.claude/skills/workflow-commands/kickoff.md +1 -1
  20. package/.claude/skills/workflow-commands/observation-ledger.md +3 -3
  21. package/.claude/skills/workflow-commands/pre-gate-cut.md +6 -1
  22. package/.claude/skills/workflow-commands/prerequisite.md +2 -2
  23. package/.claude/skills/workflow-commands/progress-watcher.md +1 -1
  24. package/.claude/skills/workflow-commands/working-tree-hold.md +1 -1
  25. package/.codex/hooks.json +2 -2
  26. package/.cursorrules +6 -41
  27. package/.github/actions/setup-node/action.yml +1 -1
  28. package/.github/workflows/auto-tag.yml +3 -4
  29. package/.github/workflows/ci.yml +32 -31
  30. package/.github/workflows/dependabot-auto-merge.yml +13 -6
  31. package/.github/workflows/publish.yml +48 -4
  32. package/.github/workflows/sonar-qube.yml +3 -0
  33. package/.pnpmfile.mjs +4 -3
  34. package/.prettierignore +8 -2
  35. package/CLAUDE.md +18 -20
  36. package/README.md +26 -19
  37. package/SECURITY.md +15 -1
  38. package/dist/CLAUDE.md +18 -20
  39. package/dist/commands/document-section-cli.js +734 -0
  40. package/dist/hooks/audit-provision.js +346 -0
  41. package/dist/hooks/batch-guard.js +5 -4
  42. package/dist/hooks/{chunk-S6X6WYKR.js → chunk-55HHG5PX.js} +1210 -711
  43. package/dist/hooks/{chunk-ZAL3NRGL.js → chunk-5VMKCCC4.js} +25 -10
  44. package/dist/hooks/chunk-5VNDSNG3.js +12 -0
  45. package/dist/hooks/{chunk-32GQTL4F.js → chunk-6HJJXRBX.js} +4 -2
  46. package/dist/hooks/chunk-CDY4NFQG.js +15 -0
  47. package/dist/hooks/chunk-ELCN5LCH.js +135 -0
  48. package/dist/hooks/{chunk-VESKRQOF.js → chunk-F56MFS4B.js} +3 -3
  49. package/dist/hooks/{chunk-UAGLLIGZ.js → chunk-FIVDHQGT.js} +7 -6
  50. package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
  51. package/dist/hooks/{chunk-G6KRCKEC.js → chunk-JUDKDCDP.js} +496 -378
  52. package/dist/hooks/{chunk-GDS5N2KB.js → chunk-NV2JRZZO.js} +718 -681
  53. package/dist/hooks/{chunk-VQLLNFNT.js → chunk-RNWHN5BP.js} +3 -3
  54. package/dist/hooks/{chunk-DO6NWWHU.js → chunk-TQNPDZNH.js} +4 -4
  55. package/dist/hooks/{chunk-I4XSL4TT.js → chunk-VC3PZPZO.js} +1 -1
  56. package/dist/hooks/codex-hook-adapter.js +14 -12
  57. package/dist/hooks/duplicate-read-guard.js +6 -5
  58. package/dist/hooks/format-edited.js +4 -3
  59. package/dist/hooks/inputs.json +217 -195
  60. package/dist/hooks/investigation-guard.js +6 -5
  61. package/dist/hooks/pretool-guard.js +12 -10
  62. package/dist/hooks/session-lang.js +2 -4
  63. package/dist/hooks/stop-guard.js +66 -58
  64. package/dist/josh.js +868 -74
  65. package/dist/version/index.d.ts +7 -2
  66. package/dist/version/index.js +152 -75
  67. package/eslint/base.js +23 -1
  68. package/eslint/rules/code-quality.js +3 -0
  69. package/eslint/rules/early-return-one-liner.js +98 -0
  70. package/eslint/rules/unicorn.js +16 -1
  71. package/package.json +7 -6
  72. package/pnpm-workspace.yaml +9 -4
  73. package/prettier/basic.d.ts +3 -1
  74. package/prettier/index.d.ts +3 -1
  75. package/prettier/static.d.ts +3 -1
  76. package/prompts/coding-standards.md +1 -1
  77. package/prompts/collaboration-workflow/file-edits.md +4 -4
  78. package/prompts/collaboration-workflow/glossary.md +1 -1
  79. package/prompts/collaboration-workflow/operating-rules.md +17 -9
  80. package/prompts/collaboration-workflow/overview.md +2 -2
  81. package/prompts/collaboration-workflow/principles.md +14 -12
  82. package/prompts/collaboration-workflow/report-format.md +1 -1
  83. package/prompts/collaboration-workflow/residency.md +1 -1
  84. package/prompts/collaboration-workflow/rule-delivery.md +6 -6
  85. package/prompts/collaboration-workflow/shell-body.md +6 -6
  86. package/prompts/collaboration-workflow/turn-batching.md +1 -1
  87. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -2
  88. package/prompts/collaboration-workflow/wip-cap.md +10 -10
  89. package/prompts/review-rubric.md +1 -2
  90. package/prompts/review.md +1 -1
  91. package/scripts/agent/agent-headless.ts +29 -0
  92. package/scripts/agent/agent-launch-environment.ts +6 -2
  93. package/scripts/agent/codex-agent-argv.ts +2 -1
  94. package/scripts/auto-ok/auto-ok-cli.ts +1 -1
  95. package/scripts/backlog/backlog-arrival.ts +3 -3
  96. package/scripts/backlog/backlog-budget.ts +1 -1
  97. package/scripts/backlog/backlog-drive-args.ts +135 -0
  98. package/scripts/backlog/backlog-drive-cli.ts +11 -137
  99. package/scripts/backlog/backlog-drive-launch.ts +39 -0
  100. package/scripts/backlog/backlog-drive-named-offer.ts +3 -5
  101. package/scripts/backlog/backlog-drive-named.ts +3 -3
  102. package/scripts/backlog/backlog-drive-offer-argv.ts +1 -1
  103. package/scripts/backlog/backlog-drive-owner.ts +1 -1
  104. package/scripts/backlog/backlog-drive-restore.ts +1 -1
  105. package/scripts/backlog/backlog-drive-retrospective.ts +1 -1
  106. package/scripts/backlog/backlog-drive.ts +1 -1
  107. package/scripts/backlog/backlog-next.ts +16 -5
  108. package/scripts/backlog/backlog-offer-cli.ts +6 -6
  109. package/scripts/backlog/backlog-offer.ts +1 -1
  110. package/scripts/backlog/backlog-plan-cli.ts +1 -1
  111. package/scripts/backlog/backlog-pool.ts +57 -11
  112. package/scripts/backlog/backlog-ready.ts +1 -1
  113. package/scripts/backlog/backlog-scope.ts +1 -1
  114. package/scripts/backlog/backlog-stalled-detect.ts +2 -2
  115. package/scripts/backlog/backlog-stalled.ts +1 -1
  116. package/scripts/build/build-codex-hooks.ts +3 -1
  117. package/scripts/build/build-commands.ts +66 -0
  118. package/scripts/build/build-hooks.ts +2 -1
  119. package/scripts/build/build-library.ts +12 -3
  120. package/scripts/build/build.ts +70 -0
  121. package/scripts/build/local-bin.ts +3 -13
  122. package/scripts/delegation/investigation-reads.ts +3 -1
  123. package/scripts/discovery/repo-origin.ts +5 -5
  124. package/scripts/doctor/doctor-consumer.ts +2 -2
  125. package/scripts/doctor/doctor-io.ts +2 -3
  126. package/scripts/document/capped-print-part.ts +30 -0
  127. package/scripts/document/capped-print.ts +3 -4
  128. package/scripts/document/document-byte-budget.ts +11 -2
  129. package/scripts/document/document-scan.ts +117 -0
  130. package/scripts/document/document-section.ts +143 -16
  131. package/scripts/document/entry-read-budget.ts +5 -3
  132. package/scripts/document/entry-read-set.ts +10 -89
  133. package/scripts/document/read-set-cost.ts +98 -0
  134. package/scripts/epic/epic-add-body.ts +1 -0
  135. package/scripts/epic/epic-audit-checks.ts +1 -1
  136. package/scripts/epic/epic-audit-rationale.ts +4 -3
  137. package/scripts/epic/epic-bundle-cli.ts +14 -3
  138. package/scripts/epic/epic-bundle-poll.ts +41 -0
  139. package/scripts/epic/epic-busy.ts +1 -1
  140. package/scripts/epic/epic-candidate-confirm.ts +3 -2
  141. package/scripts/epic/epic-classify.ts +1 -1
  142. package/scripts/epic/epic-cli-argv.ts +78 -46
  143. package/scripts/epic/epic-cli-remove.ts +11 -19
  144. package/scripts/epic/epic-cli.ts +74 -73
  145. package/scripts/epic/epic-close-comment.ts +1 -1
  146. package/scripts/epic/epic-close.ts +1 -1
  147. package/scripts/epic/epic-cross-repo.ts +5 -5
  148. package/scripts/epic/epic-graph.ts +1 -1
  149. package/scripts/epic/epic-index.ts +1 -1
  150. package/scripts/epic/epic-issue.ts +2 -6
  151. package/scripts/epic/epic-lane-offer.ts +2 -2
  152. package/scripts/epic/epic-next.ts +29 -42
  153. package/scripts/epic/epic-solo-stale.ts +1 -1
  154. package/scripts/epic/epic-triage.ts +8 -4
  155. package/scripts/epic/epic-validate.ts +1 -1
  156. package/scripts/followup/git-followup-finish.ts +2 -2
  157. package/scripts/followup/git-followup-label.ts +2 -2
  158. package/scripts/followup/git-followup-merged.ts +2 -2
  159. package/scripts/followup/git-pr-followup-checks.ts +142 -0
  160. package/scripts/followup/git-pr-followup.ts +4 -129
  161. package/scripts/gate/core-budget.ts +69 -1
  162. package/scripts/gate/gate-plan.ts +47 -7
  163. package/scripts/gate/gate-skip.ts +38 -7
  164. package/scripts/gate/hook-gate-reuse.ts +12 -10
  165. package/scripts/gate/pre-commit-type-check.ts +4 -1
  166. package/scripts/gate/project-checks.ts +6 -9
  167. package/scripts/gate/type-check-step.ts +2 -3
  168. package/scripts/gate/verification-gate.ts +27 -8
  169. package/scripts/gh/git-gh-api-path.ts +2 -2
  170. package/scripts/gh/git-gh-issue-rest.ts +3 -3
  171. package/scripts/gh/git-gh-issue-write.ts +53 -7
  172. package/scripts/gh/git-gh-pr-read.ts +64 -19
  173. package/scripts/gh/git-gh-pr-rest.ts +1 -1
  174. package/scripts/gh/git-gh-pr.ts +5 -1
  175. package/scripts/gh/git-pr-ai-review.ts +1 -1
  176. package/scripts/gh/git-pr-checks-eval.ts +1 -3
  177. package/scripts/gh/git-pr-checks-parse.ts +1 -1
  178. package/scripts/gh/git-pr-checks-watch.ts +6 -5
  179. package/scripts/gh/git-pr-checks.ts +7 -5
  180. package/scripts/gh/git-pr-coderabbit.ts +1 -1
  181. package/scripts/gh/git-pr-error.ts +1 -3
  182. package/scripts/gh/git-pr.ts +9 -13
  183. package/scripts/gh/github-issue-url.ts +2 -2
  184. package/scripts/git/constants.ts +2 -6
  185. package/scripts/git/git-command.ts +20 -218
  186. package/scripts/git/git-diff-reads.ts +225 -0
  187. package/scripts/git/git-error.ts +4 -12
  188. package/scripts/git/git-pre-push-hook.ts +139 -0
  189. package/scripts/git/git-prompt.ts +2 -6
  190. package/scripts/git/git-spawn-sync.ts +3 -1
  191. package/scripts/git/git-spawn.ts +1 -1
  192. package/scripts/git/git-staging.ts +4 -6
  193. package/scripts/git/git-status.ts +3 -9
  194. package/scripts/hooks/codex-hook-adapter.ts +1 -1
  195. package/scripts/hooks/edited-cspell.ts +2 -4
  196. package/scripts/hooks/format-edited-file.ts +3 -12
  197. package/scripts/hooks/pre-push-unit.ts +31 -9
  198. package/scripts/hooks/reserved-run.ts +2 -2
  199. package/scripts/hooks/run-hook.sh +5 -4
  200. package/scripts/hooks/stop-guard.ts +5 -3
  201. package/scripts/init/init-actions.ts +0 -5
  202. package/scripts/init/init-ai-copy.ts +13 -2
  203. package/scripts/init/init-logic-json-merge.ts +19 -0
  204. package/scripts/init/init-logic-secretlint.ts +7 -7
  205. package/scripts/init/init-logic-workspace.ts +41 -2
  206. package/scripts/init/init-logic.ts +16 -26
  207. package/scripts/init/init.ts +5 -15
  208. package/scripts/init/initial-commit.ts +4 -4
  209. package/scripts/init/kit-development-versions.ts +40 -0
  210. package/scripts/init/kit-setup-state.ts +2 -2
  211. package/scripts/init/start-exec.ts +68 -19
  212. package/scripts/init/start-setup-pr.ts +9 -9
  213. package/scripts/init/start-steps.ts +6 -10
  214. package/scripts/issue/defect-rate-cli.ts +1 -1
  215. package/scripts/issue/git-next-issues.ts +1 -1
  216. package/scripts/issue/issue-auto-ok.ts +1 -1
  217. package/scripts/issue/issue-backlinks-cli.ts +2 -2
  218. package/scripts/issue/issue-backlinks.ts +9 -9
  219. package/scripts/issue/issue-comment-cli.ts +19 -13
  220. package/scripts/issue/issue-file-cli.ts +45 -18
  221. package/scripts/issue/issue-file.ts +24 -1
  222. package/scripts/issue/issue-scout-closed.ts +1 -1
  223. package/scripts/issue/issue-state-cli.ts +30 -74
  224. package/scripts/josh/josh-command-map.ts +2 -0
  225. package/scripts/josh/josh-command-types.ts +11 -0
  226. package/scripts/josh/josh-commands-ai.ts +4 -4
  227. package/scripts/josh/josh-commands-backlog.ts +4 -1
  228. package/scripts/josh/josh-commands-development.ts +10 -0
  229. package/scripts/josh/josh-commands-document.ts +1 -0
  230. package/scripts/josh/josh-commands-lane.ts +22 -0
  231. package/scripts/josh/josh-commands-maintenance.ts +4 -4
  232. package/scripts/josh/josh-commands-run.ts +12 -7
  233. package/scripts/josh/josh-in-process.ts +28 -9
  234. package/scripts/josh/josh-logic.ts +33 -15
  235. package/scripts/josh/process-identity.ts +1 -3
  236. package/scripts/josh/session-language-cli.ts +3 -4
  237. package/scripts/lane/lane-await.ts +5 -1
  238. package/scripts/lane/lane-child-invocation.ts +2 -2
  239. package/scripts/lane/lane-cli.ts +3 -3
  240. package/scripts/lane/lane-dispatch.ts +14 -10
  241. package/scripts/lane/lane-environment.ts +5 -1
  242. package/scripts/lane/lane-handoff.ts +1 -1
  243. package/scripts/lane/lane-install.ts +2 -13
  244. package/scripts/lane/lane-launch-cli.ts +15 -1
  245. package/scripts/lane/lane-ledger.ts +145 -0
  246. package/scripts/lane/lane-leftover.ts +1 -1
  247. package/scripts/lane/lane-load.ts +108 -0
  248. package/scripts/lane/lane-measure-cli.ts +149 -0
  249. package/scripts/lane/lane-occupancy.ts +3 -0
  250. package/scripts/lane/lane-reap.ts +1 -3
  251. package/scripts/lane/lane-relaunch.ts +1 -1
  252. package/scripts/lane/lane-stats.ts +224 -0
  253. package/scripts/lane/lane-vacant.ts +36 -0
  254. package/scripts/lane/openai-lane-ship.ts +2 -2
  255. package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
  256. package/scripts/lane/openai-lane-supervisor.ts +7 -165
  257. package/scripts/lane/openai-review-broker.ts +2 -4
  258. package/scripts/lib/ancestor-directories.ts +35 -0
  259. package/scripts/lib/animation-helpers.ts +2 -2
  260. package/scripts/lib/buffered-process.ts +3 -7
  261. package/scripts/lib/cli-flags.ts +71 -1
  262. package/scripts/lib/spawn-exit.ts +2 -2
  263. package/scripts/lib/timeouts.ts +35 -2
  264. package/scripts/lines/line-budget.ts +4 -6
  265. package/scripts/notify/telegram-test.ts +6 -0
  266. package/scripts/observations/observation-ledger-line.ts +6 -6
  267. package/scripts/observations/observation-ledger-migrate.ts +55 -4
  268. package/scripts/observations/observation-ledger.ts +26 -7
  269. package/scripts/overrides/lockfile-overrides.ts +1 -1
  270. package/scripts/overrides/overrides-logic.ts +1 -1
  271. package/scripts/propagate/propagate-git.ts +1 -1
  272. package/scripts/propagate/propagate-publish.ts +2 -2
  273. package/scripts/propagate/propagate-steps.ts +20 -15
  274. package/scripts/propagate/propagate.ts +36 -27
  275. package/scripts/refactor/import-graph.ts +18 -1
  276. package/scripts/refactor/refactor-lint.ts +4 -4
  277. package/scripts/repo/repository-labels.ts +2 -2
  278. package/scripts/repo/required-checks-logic.ts +1 -3
  279. package/scripts/review/live-evidence.ts +2 -4
  280. package/scripts/review/review-brief-cli.ts +1 -1
  281. package/scripts/review/review-finding-ledger.ts +1 -1
  282. package/scripts/review/review-findings-cli.ts +1 -3
  283. package/scripts/rules/decision-oracle-batch.ts +128 -0
  284. package/scripts/rules/decision-oracle-tokens.ts +21 -0
  285. package/scripts/rules/decision-oracle.ts +8 -120
  286. package/scripts/rules/delivered-rules.ts +26 -182
  287. package/scripts/rules/direct-filing.ts +1 -1
  288. package/scripts/rules/early-heartbeat.ts +3 -3
  289. package/scripts/rules/git-argv.ts +1 -1
  290. package/scripts/rules/git-force.ts +1 -1
  291. package/scripts/rules/implementation-cut.ts +2 -2
  292. package/scripts/rules/index-guard.ts +1 -1
  293. package/scripts/rules/issue-comments.ts +118 -0
  294. package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
  295. package/scripts/rules/lane-background.ts +2 -2
  296. package/scripts/rules/lane-park.ts +18 -0
  297. package/scripts/rules/lane-split-park.ts +128 -0
  298. package/scripts/rules/oracle-firing.ts +1 -1
  299. package/scripts/rules/poll-loop.ts +1 -1
  300. package/scripts/rules/pre-gate-cut.ts +1 -1
  301. package/scripts/rules/protected-files.ts +59 -4
  302. package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
  303. package/scripts/rules/shell-body-trigger.ts +1 -1
  304. package/scripts/rules/stop-rules.ts +18 -7
  305. package/scripts/rules/test-declared-commit.ts +4 -4
  306. package/scripts/rules/wip-cap.ts +63 -0
  307. package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
  308. package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
  309. package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
  310. package/scripts/run/{run-carry.ts → carry/run-carry.ts} +1 -1
  311. package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
  312. package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +2 -2
  313. package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
  314. package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
  315. package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
  316. package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
  317. package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
  318. package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +16 -2
  319. package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
  320. package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
  321. package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
  322. package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
  323. package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
  324. package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
  325. package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
  326. package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
  327. package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
  328. package/scripts/run/run-ending.ts +2 -2
  329. package/scripts/run/run-halfrun-resume.ts +6 -1
  330. package/scripts/run/run-headless.ts +1 -1
  331. package/scripts/run/run-invocation.ts +85 -84
  332. package/scripts/run/run-liveness.ts +1 -1
  333. package/scripts/run/run-parent-cut-hook.ts +1 -1
  334. package/scripts/run/run-preflight.ts +6 -6
  335. package/scripts/run/run-prep-cli.ts +1 -1
  336. package/scripts/run/run-prrun-resume.ts +1 -1
  337. package/scripts/run/run-report-cli.ts +4 -4
  338. package/scripts/run/run-report.ts +2 -2
  339. package/scripts/run/run-status-cli.ts +1 -1
  340. package/scripts/run/run-step-cli.ts +3 -3
  341. package/scripts/run/run-step.ts +2 -2
  342. package/scripts/run/run-stop-notify.ts +1 -1
  343. package/scripts/run/run-stranded-detect.ts +4 -4
  344. package/scripts/run/run-stranded.ts +2 -2
  345. package/scripts/run/run-watcher-guard-cli.ts +1 -1
  346. package/scripts/run/run-watcher-guard.ts +5 -3
  347. package/scripts/run/run-watcher-hook.ts +1 -1
  348. package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +2 -2
  349. package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
  350. package/scripts/run/ship/run-ship-lock.ts +50 -0
  351. package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
  352. package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
  353. package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +4 -3
  354. package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
  355. package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
  356. package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +11 -12
  357. package/scripts/run/ship/run-ship-stop-text.ts +13 -0
  358. package/scripts/run/{run-ship-sync.ts → ship/run-ship-sync.ts} +25 -11
  359. package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
  360. package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +23 -8
  361. package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +3 -3
  362. package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +59 -8
  363. package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
  364. package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +8 -2
  365. package/scripts/run/wake/run-wake-restart.ts +58 -0
  366. package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
  367. package/scripts/run/{run-wake.ts → wake/run-wake.ts} +1 -1
  368. package/scripts/safe-chain/preinstall-command.ts +20 -21
  369. package/scripts/safe-chain/project-config.ts +19 -15
  370. package/scripts/sync/sync-ai-files.ts +1 -1
  371. package/scripts/sync/sync-configs.ts +2 -1
  372. package/scripts/sync/sync.ts +15 -2
  373. package/scripts/test/unit-worker-share.ts +1 -0
  374. package/scripts/time-runtime/time-background.ts +5 -1
  375. package/scripts/time-runtime/time-instant.ts +3 -1
  376. package/scripts/time-runtime/time-spans.ts +6 -139
  377. package/scripts/time-runtime/time-tool-call.ts +160 -0
  378. package/scripts/version/effective-upstream.ts +2 -20
  379. package/scripts/version/latest-corepack.ts +17 -17
  380. package/scripts/version/latest-regression.ts +1 -1
  381. package/scripts/version/latest-update.ts +4 -4
  382. package/scripts/version/npm-registry.ts +20 -5
  383. package/scripts/version/publishable-range-check.ts +2 -2
  384. package/scripts/version/publishable-range.ts +1 -1
  385. package/scripts/version/release-age.ts +48 -38
  386. package/scripts/version/release-hold.ts +1 -1
  387. package/scripts/version/safe-chain-version-update.ts +40 -0
  388. package/scripts/version/upgrade-shell-command.ts +48 -8
  389. package/scripts/version/version-check-logic.ts +23 -14
  390. package/scripts/version/version-commands.ts +23 -11
  391. package/scripts/version/version-remote.ts +28 -13
  392. package/templates/pnpm-workspace.basic.yaml +4 -0
  393. package/templates/workflows/dependabot-auto-merge.yml +5 -5
  394. package/templates/workflows/github-release.yml +2 -0
  395. package/scripts/build/build-config-merge.ts +0 -27
  396. package/scripts/build/build-managed-marker.ts +0 -27
  397. package/scripts/build/build-self-sync-guard.ts +0 -27
  398. package/scripts/build/build-version.ts +0 -26
  399. package/scripts/version/preinstall-version-update.ts +0 -86
  400. /package/scripts/git/{schemas.ts → git-schemas.ts} +0 -0
  401. /package/scripts/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
  402. /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
  403. /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
  404. /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
  405. /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
  406. /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
  407. /package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +0 -0
  408. /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
  409. /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
  410. /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
  411. /package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +0 -0
  412. /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
  413. /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
  414. /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
  415. /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
  416. /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
  417. /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
  418. /package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +0 -0
  419. /package/scripts/run/{run-ship-pre-detach.ts → ship/run-ship-pre-detach.ts} +0 -0
  420. /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
  421. /package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +0 -0
  422. /package/scripts/run/{run-ship.ts → ship/run-ship.ts} +0 -0
  423. /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
  424. /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
  425. /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
@@ -0,0 +1,142 @@
1
+ import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { git_gh_helpers } from '#scripts/gh/git-gh-helpers'
3
+ import {
4
+ DEFAULT_STABLE_READS,
5
+ git_pr_checks,
6
+ WATCH_CONFIRMED_STABLE_READS,
7
+ } from '#scripts/gh/git-pr-checks'
8
+ import { is_coderabbit_check } from '#scripts/gh/git-pr-checks-eval'
9
+ import { CHECK_STATUS_PASS, type PrStateSnapshot } from '#scripts/gh/git-pr-checks-parse'
10
+ import { json_value } from '#scripts/lib/json-value'
11
+
12
+ // The check wait and the notes read off its snapshot — split out of `git-pr-followup.ts` when it
13
+ // reached its line limit (joshuafolkken/kit#3264). `git-pr-followup` re-exports each name it always
14
+ // exported, so the move changed no call site and no suite that imports from it.
15
+
16
+ // Named so a test can pin the note without restating it (joshuafolkken/kit#999).
17
+ const WATCH_FAILED_NOTE = 'pr checks --watch failed; falling through to polling'
18
+
19
+ // The watch is a look ahead, not a gate. It fails when **any** check has failed — CodeRabbit
20
+ // included — and letting that escape ended the run before `evaluate_pr_state` could apply kit#753's
21
+ // CodeRabbit exemption at all. It only ever bit where every check finished inside the two-minute
22
+ // window, so a repository with a slow E2E never saw it and a fast one always would
23
+ // (joshuafolkken/kit#999). What the watch is has since changed — `gh pr checks --watch` went through
24
+ // GraphQL, so joshuafolkken/kit#1028 replaced it with the same poll loop bounded to two minutes —
25
+ // but its place here has not.
26
+ //
27
+ // Falling through costs nothing: whether the merge may proceed is decided in one place below, and a
28
+ // genuinely failing non-CodeRabbit check still ends the wait on the first poll by kit#990's
29
+ // fast-fail rather than by that failure.
30
+ // The watch fails for two different reasons — "a check failed" and "no checks reported on this
31
+ // branch" — and its own error does not say which. The pull request itself can: a failed check leaves
32
+ // a rollup, and a branch with no checks leaves it empty. Falling through on the empty case would
33
+ // trade a failure reported in seconds for the whole budget spent waiting on a required check that is
34
+ // missing rather than pending, so that one is rethrown and the old behavior kept.
35
+ //
36
+ // **Read from the raw payload, not from `parse_pr_state_snapshot`.** That parser never throws: a
37
+ // malformed answer or a schema mismatch degrades to `rollup: []`, which is indistinguishable there
38
+ // from a branch that genuinely has no checks. Rethrowing on it would put an unreadable answer back
39
+ // on the path this whole change exists to remove. So the question asked here is the narrow one —
40
+ // *is this definitely an empty rollup* — and every other outcome falls through.
41
+ function reads_as_empty_rollup(raw_json: string): boolean {
42
+ const parsed = json_value.parse_or_undefined(raw_json)
43
+
44
+ if (typeof parsed !== 'object' || parsed === null || !('statusCheckRollup' in parsed)) {
45
+ return false
46
+ }
47
+
48
+ const { statusCheckRollup: rollup } = parsed
49
+
50
+ return Array.isArray(rollup) && rollup.length === 0
51
+ }
52
+
53
+ // The read throws rather than answering `undefined`, so the `catch` is its only failure path.
54
+ //
55
+ // The **checks** half, not the whole snapshot: the question here is about `statusCheckRollup` alone,
56
+ // and reading the review listing to answer it paged a whole conversation for nothing
57
+ // (joshuafolkken/kit#1043).
58
+ async function has_no_checks(branch_name: string): Promise<boolean> {
59
+ try {
60
+ const checks = await git_gh_command.pr_get_checks_snapshot(branch_name)
61
+
62
+ return reads_as_empty_rollup(checks.snapshot_json)
63
+ } catch {
64
+ // The read refines the swallow; it is not a gate of its own. An answer it cannot get is not
65
+ // evidence of anything, so prefer falling through — the poll below reads the same state.
66
+ return false
67
+ }
68
+ }
69
+
70
+ async function handle_watch_failure(branch_name: string, error: unknown): Promise<void> {
71
+ if (await has_no_checks(branch_name)) throw error
72
+
73
+ // Swallowed, but never silently: the reason a run stopped early used to be this line, so it
74
+ // stays visible even though it no longer decides anything.
75
+ console.info(`⚠️ ${WATCH_FAILED_NOTE}: ${git_gh_helpers.get_error_message_with_stderr(error)}`)
76
+ }
77
+
78
+ // **Returns whether the watch confirmed completion** (joshuafolkken/kit#2029): `true` only when
79
+ // `pr_checks_watch` saw every check finish, `false` when it timed out with checks still pending or
80
+ // fell through to polling on a swallowed failure. `run_checks` reads it to decide how many stable poll
81
+ // reads the wait still needs. A timed-out watch (`timed_out: true`) never spanned the pending→settled
82
+ // window, so it must keep the full stable-read window rather than claim the confirmation. A rethrown
83
+ // failure — the empty-rollup case — never reaches a caller, so it needs no answer here.
84
+ async function watch_before_polling(branch_name: string): Promise<boolean> {
85
+ console.info('')
86
+ console.info('📊 Watching PR checks...')
87
+
88
+ try {
89
+ const result = await git_gh_command.pr_checks_watch(branch_name)
90
+
91
+ return !result.timed_out
92
+ } catch (error) {
93
+ await handle_watch_failure(branch_name, error)
94
+
95
+ return false
96
+ }
97
+ }
98
+
99
+ // **A confirmed watch lowers the poll's stable-read requirement to one** (joshuafolkken/kit#2029). The
100
+ // watch is the first confirmation that every check settled, so the poll that follows need only agree
101
+ // once rather than twice — which removes the extra interval `followup` spent re-proving a settled run.
102
+ // A skipped or fallen-through watch keeps `DEFAULT_STABLE_READS`, and a check that turned red is still
103
+ // caught: the poll evaluates the full merge gate and throws on failure before any stable read counts.
104
+ async function run_checks(input: {
105
+ branch_name: string
106
+ is_skip_watch: boolean
107
+ }): Promise<PrStateSnapshot> {
108
+ const is_watch_confirmed = !input.is_skip_watch && (await watch_before_polling(input.branch_name))
109
+ const stable_reads = is_watch_confirmed ? WATCH_CONFIRMED_STABLE_READS : DEFAULT_STABLE_READS
110
+
111
+ return await git_pr_checks.wait_for_pr_success(input.branch_name, stable_reads)
112
+ }
113
+
114
+ // Temporary (kit#753): record every CodeRabbit check that was not passing when the merge gate
115
+ // opened, so a merge shipped without CodeRabbit review stays auditable.
116
+ function read_coderabbit_skip_notes(snapshot: PrStateSnapshot): Array<string> {
117
+ return snapshot.rollup
118
+ .filter((check) => is_coderabbit_check(check.name))
119
+ .filter((check) => check.status !== CHECK_STATUS_PASS)
120
+ .map(
121
+ (check) =>
122
+ `CodeRabbit check skipped (kit#753): ${check.name} was ${check.status} at merge time`,
123
+ )
124
+ }
125
+
126
+ // The skip is printed where the run is being watched, not only carried into the Telegram body it
127
+ // ends up in. A merge that went ahead while CodeRabbit had not come back is the one thing kit#753's
128
+ // exemption trades away, and it used to be invisible in `followup`'s own output
129
+ // (joshuafolkken/kit#1217).
130
+ function log_skip_notes(notes: ReadonlyArray<string>): void {
131
+ for (const note of notes) {
132
+ console.info(`⏭ ${note}`)
133
+ }
134
+ }
135
+
136
+ const git_pr_followup_checks = {
137
+ run_checks,
138
+ read_coderabbit_skip_notes,
139
+ log_skip_notes,
140
+ }
141
+
142
+ export { git_pr_followup_checks, WATCH_FAILED_NOTE }
@@ -1,18 +1,9 @@
1
1
  import { git_gh_command } from '#scripts/gh/git-gh-command'
2
- import { git_gh_helpers } from '#scripts/gh/git-gh-helpers'
3
2
  import { git_pr_ai_review, type TelegramContext } from '#scripts/gh/git-pr-ai-review'
4
- import {
5
- DEFAULT_STABLE_READS,
6
- git_pr_checks,
7
- WATCH_CONFIRMED_STABLE_READS,
8
- } from '#scripts/gh/git-pr-checks'
9
- import { is_coderabbit_check } from '#scripts/gh/git-pr-checks-eval'
10
- import { CHECK_STATUS_PASS, type PrStateSnapshot } from '#scripts/gh/git-pr-checks-parse'
11
3
  import { git_pr_coderabbit } from '#scripts/gh/git-pr-coderabbit'
12
4
  import { git_pr_managed_config } from '#scripts/gh/git-pr-managed-config'
13
5
  import { github_issue_url } from '#scripts/gh/github-issue-url'
14
6
  import { git_closes_keyword } from '#scripts/git/git-closes-keyword'
15
- import { json_value } from '#scripts/lib/json-value'
16
7
  import type { GitNotifyConfig } from '#scripts/notify/git-notify'
17
8
  import {
18
9
  telegram_notify,
@@ -22,12 +13,12 @@ import {
22
13
  import { git_followup_merged } from './git-followup-merged'
23
14
  import { git_followup_pending } from './git-followup-pending'
24
15
  import { git_followup_stages, type StageLog } from './git-followup-stages'
16
+ import { git_pr_followup_checks } from './git-pr-followup-checks'
25
17
  import { git_pr_followup_wrapup } from './git-pr-followup-wrapup'
26
18
 
27
19
  const { STAGE, lap } = git_followup_stages
20
+ const { run_checks, read_coderabbit_skip_notes, log_skip_notes } = git_pr_followup_checks
28
21
 
29
- // Named so a test can pin the note without restating it (joshuafolkken/kit#999).
30
- const WATCH_FAILED_NOTE = 'pr checks --watch failed; falling through to polling'
31
22
  const REPO_NAME_SEPARATOR = '/'
32
23
 
33
24
  function parse_repo_name(name_with_owner: string | undefined): string | undefined {
@@ -107,123 +98,6 @@ async function warn_if_missing_closes(branch_name: string): Promise<string | und
107
98
  return undefined
108
99
  }
109
100
 
110
- // The watch is a look ahead, not a gate. It fails when **any** check has failed — CodeRabbit
111
- // included — and letting that escape ended the run before `evaluate_pr_state` could apply kit#753's
112
- // CodeRabbit exemption at all. It only ever bit where every check finished inside the two-minute
113
- // window, so a repository with a slow E2E never saw it and a fast one always would
114
- // (joshuafolkken/kit#999). What the watch is has since changed — `gh pr checks --watch` went through
115
- // GraphQL, so joshuafolkken/kit#1028 replaced it with the same poll loop bounded to two minutes —
116
- // but its place here has not.
117
- //
118
- // Falling through costs nothing: whether the merge may proceed is decided in one place below, and a
119
- // genuinely failing non-CodeRabbit check still ends the wait on the first poll by kit#990's
120
- // fast-fail rather than by that failure.
121
- // The watch fails for two different reasons — "a check failed" and "no checks reported on this
122
- // branch" — and its own error does not say which. The pull request itself can: a failed check leaves
123
- // a rollup, and a branch with no checks leaves it empty. Falling through on the empty case would
124
- // trade a failure reported in seconds for the whole budget spent waiting on a required check that is
125
- // missing rather than pending, so that one is rethrown and the old behavior kept.
126
- //
127
- // **Read from the raw payload, not from `parse_pr_state_snapshot`.** That parser never throws: a
128
- // malformed answer or a schema mismatch degrades to `rollup: []`, which is indistinguishable there
129
- // from a branch that genuinely has no checks. Rethrowing on it would put an unreadable answer back
130
- // on the path this whole change exists to remove. So the question asked here is the narrow one —
131
- // *is this definitely an empty rollup* — and every other outcome falls through.
132
- function reads_as_empty_rollup(raw_json: string): boolean {
133
- const parsed = json_value.parse_or_undefined(raw_json)
134
-
135
- if (typeof parsed !== 'object' || parsed === null || !('statusCheckRollup' in parsed)) {
136
- return false
137
- }
138
-
139
- const { statusCheckRollup: rollup } = parsed
140
-
141
- return Array.isArray(rollup) && rollup.length === 0
142
- }
143
-
144
- // The read throws rather than answering `undefined`, so the `catch` is its only failure path.
145
- //
146
- // The **checks** half, not the whole snapshot: the question here is about `statusCheckRollup` alone,
147
- // and reading the review listing to answer it paged a whole conversation for nothing
148
- // (joshuafolkken/kit#1043).
149
- async function has_no_checks(branch_name: string): Promise<boolean> {
150
- try {
151
- const checks = await git_gh_command.pr_get_checks_snapshot(branch_name)
152
-
153
- return reads_as_empty_rollup(checks.snapshot_json)
154
- } catch {
155
- // The read refines the swallow; it is not a gate of its own. An answer it cannot get is not
156
- // evidence of anything, so prefer falling through — the poll below reads the same state.
157
- return false
158
- }
159
- }
160
-
161
- async function handle_watch_failure(branch_name: string, error: unknown): Promise<void> {
162
- if (await has_no_checks(branch_name)) throw error
163
-
164
- // Swallowed, but never silently: the reason a run stopped early used to be this line, so it
165
- // stays visible even though it no longer decides anything.
166
- console.info(`⚠️ ${WATCH_FAILED_NOTE}: ${git_gh_helpers.get_error_message_with_stderr(error)}`)
167
- }
168
-
169
- // **Returns whether the watch confirmed completion** (joshuafolkken/kit#2029): `true` only when
170
- // `pr_checks_watch` saw every check finish, `false` when it timed out with checks still pending or
171
- // fell through to polling on a swallowed failure. `run_checks` reads it to decide how many stable poll
172
- // reads the wait still needs. A timed-out watch (`timed_out: true`) never spanned the pending→settled
173
- // window, so it must keep the full stable-read window rather than claim the confirmation. A rethrown
174
- // failure — the empty-rollup case — never reaches a caller, so it needs no answer here.
175
- async function watch_before_polling(branch_name: string): Promise<boolean> {
176
- console.info('')
177
- console.info('📊 Watching PR checks...')
178
-
179
- try {
180
- const result = await git_gh_command.pr_checks_watch(branch_name)
181
-
182
- return !result.timed_out
183
- } catch (error) {
184
- await handle_watch_failure(branch_name, error)
185
-
186
- return false
187
- }
188
- }
189
-
190
- // **A confirmed watch lowers the poll's stable-read requirement to one** (joshuafolkken/kit#2029). The
191
- // watch is the first confirmation that every check settled, so the poll that follows need only agree
192
- // once rather than twice — which removes the extra interval `followup` spent re-proving a settled run.
193
- // A skipped or fallen-through watch keeps `DEFAULT_STABLE_READS`, and a check that turned red is still
194
- // caught: the poll evaluates the full merge gate and throws on failure before any stable read counts.
195
- async function run_checks(input: {
196
- branch_name: string
197
- is_skip_watch: boolean
198
- }): Promise<PrStateSnapshot> {
199
- const is_watch_confirmed = !input.is_skip_watch && (await watch_before_polling(input.branch_name))
200
- const stable_reads = is_watch_confirmed ? WATCH_CONFIRMED_STABLE_READS : DEFAULT_STABLE_READS
201
-
202
- return await git_pr_checks.wait_for_pr_success(input.branch_name, stable_reads)
203
- }
204
-
205
- // Temporary (kit#753): record every CodeRabbit check that was not passing when the merge gate
206
- // opened, so a merge shipped without CodeRabbit review stays auditable.
207
- function read_coderabbit_skip_notes(snapshot: PrStateSnapshot): Array<string> {
208
- return snapshot.rollup
209
- .filter((check) => is_coderabbit_check(check.name))
210
- .filter((check) => check.status !== CHECK_STATUS_PASS)
211
- .map(
212
- (check) =>
213
- `CodeRabbit check skipped (kit#753): ${check.name} was ${check.status} at merge time`,
214
- )
215
- }
216
-
217
- // The skip is printed where the run is being watched, not only carried into the Telegram body it
218
- // ends up in. A merge that went ahead while CodeRabbit had not come back is the one thing kit#753's
219
- // exemption trades away, and it used to be invisible in `followup`'s own output
220
- // (joshuafolkken/kit#1217).
221
- function log_skip_notes(notes: ReadonlyArray<string>): void {
222
- for (const note of notes) {
223
- console.info(`⏭ ${note}`)
224
- }
225
- }
226
-
227
101
  // **The one read of the four that can depend on another.** Without an issue number on the command
228
102
  // line it is the pull request body that names it (joshuafolkken/kit#1539), so the title read waits
229
103
  // for exactly that one and for nothing else. With a number in hand the `??` short-circuits and the
@@ -451,7 +325,6 @@ const git_pr_followup = {
451
325
 
452
326
  export {
453
327
  git_pr_followup,
454
- WATCH_FAILED_NOTE,
455
328
  run_checks,
456
329
  build_issue_url,
457
330
  parse_repo_name,
@@ -464,3 +337,5 @@ export {
464
337
  }
465
338
  export type { FollowupInput }
466
339
  export type { TelegramContext } from '#scripts/gh/git-pr-ai-review'
340
+
341
+ export { WATCH_FAILED_NOTE } from './git-pr-followup-checks'
@@ -6,6 +6,7 @@ import { PLATFORM_TEMP_ROOT } from '#scripts/josh/platform-temporary'
6
6
  import { process_identity } from '#scripts/josh/process-identity'
7
7
  import { process_owner_schema } from '#scripts/josh/process-owner'
8
8
  import { stamp_file } from '#scripts/josh/stamp-file'
9
+ import { unit_worker_share } from '#scripts/test/unit-worker-share'
9
10
  import { z } from 'zod'
10
11
 
11
12
  // A machine-wide weighted core budget with admission control (joshuafolkken/kit#2351).
@@ -46,6 +47,26 @@ const WAIT_CAP_MS = 120_000
46
47
  // The head of the ledger is always admitted, so the smallest budget worth planning against is one core.
47
48
  const MIN_BUDGET = 1
48
49
 
50
+ // **The cores each heavy tool occupies, declared once for both places that reserve them**
51
+ // (joshuafolkken/kit#3345). `gate-plan.ts`'s checks and the `josh` commands that run the same tools
52
+ // directly read these, so a direct `josh lint` claims what the gate's lint claims. The lint, type check
53
+ // and spell check numbers are `gate-plan.ts`'s measured table, floored. The whole-tree eslint scan
54
+ // behind `josh lines` and `josh refactor:scan` runs in one process: measured cold on the 11-core
55
+ // machine on 2026-10-06, 113s of CPU over 98s of wall, so it gets one core.
56
+ const CORE_WEIGHTS = {
57
+ lint: 2,
58
+ type_check: 1,
59
+ spell_check: 1,
60
+ eslint_scan: 1,
61
+ } as const
62
+
63
+ // **The environment mark that says a parent already holds this process's cores**
64
+ // (joshuafolkken/kit#3345). `josh gate` reserves per check and then spawns `pnpm josh lint`. Without
65
+ // the mark, that child's own dispatch would claim a second place for the cores its parent already
66
+ // holds. Every holder sets it, and every child inherits it through the environment and claims nothing.
67
+ const HELD_KEY = 'JOSH_CORE_RESERVED'
68
+ const HELD_VALUE = '1'
69
+
49
70
  const reservation_schema = process_owner_schema.extend({
50
71
  weight: z.number(),
51
72
  claimed_at: z.number(),
@@ -133,7 +154,7 @@ function live_reservations(directory: string = PLATFORM_TEMP_ROOT): Array<LiveRe
133
154
  return marker_files(directory).flatMap((name) => {
134
155
  const reservation = live_reservation(path.join(directory, name))
135
156
 
136
- return reservation === undefined ? [] : [{ key: name, reservation }]
157
+ return reservation === undefined ? [] : { key: name, reservation }
137
158
  })
138
159
  }
139
160
 
@@ -272,14 +293,61 @@ async function with_core_reservation<T>(
272
293
  }
273
294
  }
274
295
 
296
+ function is_held(): boolean {
297
+ return process.env[HELD_KEY] === HELD_VALUE
298
+ }
299
+
300
+ // A function of its own so the restoring write does not sit after an `await`, where
301
+ // `require-atomic-updates` reads any write to `process` as a possible race — the reason `josh.ts`
302
+ // gives `record_exit_code`.
303
+ function set_held_mark(value: string): void {
304
+ process.env[HELD_KEY] = value
305
+ }
306
+
307
+ // Marks this process, and every child it spawns, as running inside a held reservation. The previous
308
+ // value comes back afterwards, so a nested holder never clears the mark its outer holder set.
309
+ async function with_held_mark<T>(run: () => Promise<T>): Promise<T> {
310
+ const previous = process.env[HELD_KEY] ?? ''
311
+
312
+ set_held_mark(HELD_VALUE)
313
+
314
+ try {
315
+ return await run()
316
+ } finally {
317
+ set_held_mark(previous)
318
+ }
319
+ }
320
+
321
+ // **A command's reservation: the gate's claim, unless a parent already holds the cores**
322
+ // (joshuafolkken/kit#3345). `josh-logic.ts` calls this around every command that declares a weight.
323
+ // Each command then only declares its weight, and no command carries a copy of the claim, run and
324
+ // release steps. A solo run is admitted on the first read, so a run with room on the machine starts
325
+ // with no wait. A command inside a unit suite claims nothing either, for the reason `reserved-run.ts`
326
+ // gives: the suite's own reservation already covers it, and a place claimed there would wait on it.
327
+ async function with_command_reservation<T>(
328
+ weight: number,
329
+ run: () => Promise<T>,
330
+ options: ReserveOptions = {},
331
+ ): Promise<T> {
332
+ if (is_held() || unit_worker_share.is_nested_run()) return await run()
333
+
334
+ return await with_core_reservation(weight, async () => await with_held_mark(run), options)
335
+ }
336
+
275
337
  const core_budget = {
338
+ CORE_WEIGHTS,
339
+ HELD_KEY,
340
+ HELD_VALUE,
276
341
  RESERVED_PREFIX,
277
342
  admitted_keys,
278
343
  admitted_load,
344
+ is_held,
279
345
  live_reservations,
280
346
  release,
281
347
  reserve,
348
+ with_command_reservation,
282
349
  with_core_reservation,
350
+ with_held_mark,
283
351
  }
284
352
 
285
353
  export type { LiveReservation, Reservation, ReserveOptions }
@@ -1,6 +1,7 @@
1
1
  import { availableParallelism } from 'node:os'
2
2
  import { josh_verdict } from '#scripts/josh/josh-verdict'
3
3
  import { unit_worker_share } from '#scripts/test/unit-worker-share'
4
+ import { core_budget } from './core-budget'
4
5
 
5
6
  // How many of the gate's checks run at once, and how wide the one elastic check may fan out —
6
7
  // both derived from the machine rather than fixed at four (joshuafolkken/kit#1258).
@@ -61,15 +62,30 @@ const WARNING_CHECKER_LABELS: ReadonlyArray<string> = [LINT_LABEL, TYPE_CHECK_LA
61
62
  // check rather than one appended after the unit suite is what keeps the plan's shape intact: the
62
63
  // `--no-unit` set stays "the whole gate minus the unit suite", and a CI runner — which has no recorded
63
64
  // transcripts — runs it as a trivial green rather than skipping it.
65
+ //
66
+ // **The three reserving weights are `core_budget.CORE_WEIGHTS`** (joshuafolkken/kit#3345), the same
67
+ // numbers the `josh lint`, `josh check` and `josh cspell:dot` commands claim when run directly.
68
+ //
69
+ // **The two zero weights were measured again for joshuafolkken/kit#3345, and they stay zero.** These are
70
+ // warm runs on the 11-core machine on 2026-10-06. `behavior` took 0.6s of wall for 0.7s of CPU, which
71
+ // is node starting up. `exports:unused` took 5.5s of wall for 9.0s of CPU, about 1.6 cores. Floored,
72
+ // that would be one core, but a reservation lasts the whole gate's plan, not the check's five seconds.
73
+ // It would lower `RESERVED_CORES` from 4 to 5 and cut the solo unit cap from 7 workers to 6 for the
74
+ // full length of the unit suite, to cover a burst that ends before lint does. Nothing has measured
75
+ // that trade, so the measured shape stays.
64
76
  const STATIC_CHECKS: ReadonlyArray<GateCheck> = [
65
- { label: LINT_LABEL, target: 'lint', reserved_cores: 2 },
66
- { label: TYPE_CHECK_LABEL, target: 'check', reserved_cores: 1 },
67
- { label: 'cspell', target: 'cspell:dot', reserved_cores: 1 },
77
+ { label: LINT_LABEL, target: 'lint', reserved_cores: core_budget.CORE_WEIGHTS.lint },
78
+ {
79
+ label: TYPE_CHECK_LABEL,
80
+ target: 'check',
81
+ reserved_cores: core_budget.CORE_WEIGHTS.type_check,
82
+ },
83
+ { label: 'cspell', target: 'cspell:dot', reserved_cores: core_budget.CORE_WEIGHTS.spell_check },
68
84
  { label: BEHAVIOR_LABEL, target: BEHAVIOR_LABEL, reserved_cores: 0 },
69
85
  // **The unused-member check reserves no core** (joshuafolkken/kit#2987): it builds one TypeScript
70
- // program and walks it single-threaded for about 5s — over well before lint and the type check, so
71
- // a reservation would only take a place from the four-core CI runner's fan-out and a worker from
72
- // the unit suite for the whole gate.
86
+ // program and walks it for about 5s, and it is over well before lint and the type check. A
87
+ // reservation would only take a place from the four-core CI runner's fan-out and a worker from the
88
+ // unit suite for the whole gate.
73
89
  { label: 'exports', target: 'exports:unused', reserved_cores: 0 },
74
90
  ]
75
91
 
@@ -242,6 +258,11 @@ function resolve_unit_worker_cap(
242
258
  return available_cores - RESERVED_CORES
243
259
  }
244
260
 
261
+ // The unit suite's weight when nothing caps its pool: the cores left once the static checks have theirs.
262
+ function solo_unit_weight(available_cores: number): number {
263
+ return Math.max(0, available_cores - RESERVED_CORES)
264
+ }
265
+
245
266
  // The cores a check reserves from the machine-wide budget while it runs (joshuafolkken/kit#2351). The
246
267
  // static checks declare their measured reservation; the unit suite declares the pool it will open,
247
268
  // which `unit_worker_cap` has already sized against the runs in flight. **This is the same table, read
@@ -256,7 +277,25 @@ function resolve_unit_worker_cap(
256
277
  function check_weight(check: GateCheck, plan: GatePlan, available_cores: number): number {
257
278
  if (check.label !== UNIT_LABEL) return check.reserved_cores
258
279
 
259
- return plan.unit_worker_cap ?? Math.max(0, available_cores - RESERVED_CORES)
280
+ return plan.unit_worker_cap ?? solo_unit_weight(available_cores)
281
+ }
282
+
283
+ // The cores a unit run started directly reserves (joshuafolkken/kit#3345): the share it will pass to
284
+ // vitest while other runs are live, and otherwise the gate's own solo unit weight. **Not the whole
285
+ // machine**, though a lone vitest sizes its own pool: FIFO admission lets a claim of every core in only
286
+ // once the ledger is empty, so a one-file `josh test:related` would wait out every other lane's gate
287
+ // and hold every later claim behind it. One weight for the suite however it is started. **Floored at one
288
+ // worker**: on a machine the static checks fill, a four-core CI runner among them, the gate's solo
289
+ // weight is zero because its siblings hold every core — a direct run has no siblings, and vitest still
290
+ // occupies a core.
291
+ function direct_unit_weight(
292
+ available_cores: number = availableParallelism(),
293
+ live_runs: number = unit_worker_share.live_run_count(),
294
+ ): number {
295
+ const weight =
296
+ unit_worker_share.current_share(available_cores, live_runs) ?? solo_unit_weight(available_cores)
297
+
298
+ return Math.max(unit_worker_share.MIN_WORKERS, weight)
260
299
  }
261
300
 
262
301
  // `availableParallelism()` rather than `cpus().length`: it reports what this process may actually
@@ -345,6 +384,7 @@ const gate_plan = {
345
384
  UNIT_LABEL,
346
385
  WARNING_CHECKER_LABELS,
347
386
  check_weight,
387
+ direct_unit_weight,
348
388
  format_gate_plan,
349
389
  format_machine,
350
390
  has_unit_check,
@@ -60,18 +60,42 @@ const FORCE_FLAG = '--force'
60
60
  //
61
61
  // A `base` that could not be read is `undefined`, and an older record carries none: both compare
62
62
  // unequal to anything, so the gate runs its checks. No base means no reuse, never reuse without one.
63
- function reusable_green_gate(
63
+ //
64
+ // **A refusal carries its reason** (joshuafolkken/kit#3307): a pre-push hook that fell back to the
65
+ // whole unit suite said nothing about why, so a merge base moved by ship's main sync and a lock file
66
+ // rewritten by the hook's own install each cost minutes before anyone could tell them apart.
67
+ type GateReuse = { stamp: FileMapStamp } | { miss: string }
68
+
69
+ function base_moved(recorded: string | undefined, base: string): string {
70
+ return `the merge base moved since the green gate (${recorded ?? 'none'} → ${base})`
71
+ }
72
+
73
+ function gate_reuse(
64
74
  tree: Record<string, string>,
65
75
  base: string | undefined,
66
76
  source?: string,
67
- ): FileMapStamp | undefined {
68
- if (base === undefined || Object.keys(tree).length === 0) return undefined
77
+ ): GateReuse {
78
+ if (base === undefined) return { miss: 'the merge base could not be read' }
79
+ if (Object.keys(tree).length === 0) return { miss: 'no file differs from the merge base' }
69
80
 
70
81
  const stamp = review_brief.matching_stamp(review_stamps.gate_stamp.read(source), tree)
71
82
 
72
- if (stamp?.base !== base) return undefined
83
+ if (stamp === undefined) return { miss: 'no green gate record matches the changed files' }
84
+ if (stamp.base !== base) return { miss: base_moved(stamp.base, base) }
85
+
86
+ return { stamp }
87
+ }
88
+
89
+ function stamp_of(reuse: GateReuse): FileMapStamp | undefined {
90
+ return 'stamp' in reuse ? reuse.stamp : undefined
91
+ }
73
92
 
74
- return stamp
93
+ function reusable_green_gate(
94
+ tree: Record<string, string>,
95
+ base: string | undefined,
96
+ source?: string,
97
+ ): FileMapStamp | undefined {
98
+ return stamp_of(gate_reuse(tree, base, source))
75
99
  }
76
100
 
77
101
  // **The sentence claims the result, and never merely the omission.** "verification skipped" on its
@@ -116,8 +140,15 @@ function format_skip(taken_at: string): string {
116
140
  })
117
141
  }
118
142
 
119
- const gate_skip = { FORCE_FLAG, format_reuse_notice, format_skip, reusable_green_gate }
143
+ const gate_skip = {
144
+ FORCE_FLAG,
145
+ format_reuse_notice,
146
+ format_skip,
147
+ gate_reuse,
148
+ reusable_green_gate,
149
+ stamp_of,
150
+ }
120
151
 
121
- export type { ReuseNotice }
152
+ export type { GateReuse, ReuseNotice }
122
153
 
123
154
  export { gate_skip }
@@ -1,7 +1,6 @@
1
1
  import { git_command } from '#scripts/git/git-command'
2
- import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
3
2
  import { observation_ledger } from '#scripts/observations/observation-ledger'
4
- import { gate_skip } from './gate-skip'
3
+ import { gate_skip, type GateReuse } from './gate-skip'
5
4
  import type { GateTree } from './gate-tree'
6
5
 
7
6
  // What the two git hooks share when they decline to re-run a check `pnpm josh gate` already passed on
@@ -94,6 +93,8 @@ interface HookReuse {
94
93
  // The hook-specific narrowing: whether what this git operation carries is the tree the record
95
94
  // describes. `is_worktree_clean` for a push, `is_index_matching_worktree` for a commit.
96
95
  is_tree_carried: boolean
96
+ // The reason a refusal on that narrowing reports, in the hook's own words (joshuafolkken/kit#3307).
97
+ carry_miss: string
97
98
  extra_arguments: ReadonlyArray<string>
98
99
  force_env: string
99
100
  // The record to read, so a test can plant one without overwriting the record a live run relies on.
@@ -102,27 +103,28 @@ interface HookReuse {
102
103
  source?: string | undefined
103
104
  }
104
105
 
105
- // The stamp rather than a boolean, for the same reason `gate_skip.reusable_green_gate` hands one back:
106
- // the caller prints `taken_at`, and a record that does not describe this tree has no timestamp worth
107
- // printing.
106
+ // The stamp rather than a boolean, for the same reason `gate_skip.gate_reuse` hands one back: the
107
+ // caller prints `taken_at`, and a record that does not describe this tree has no timestamp worth
108
+ // printing. A refusal carries its reason instead (joshuafolkken/kit#3307).
108
109
  //
109
110
  // **Any argument at all refuses the reuse**, not only the force flag. A caller who narrowed the run
110
111
  // asked for that run rather than for a recorded result about a whole tree, and a hook's own line passes
111
112
  // none, so this costs it nothing.
112
- function reusable_green_hook(input: HookReuse): FileMapStamp | undefined {
113
- if (input.extra_arguments.length > 0 || is_force_requested(input.force_env)) return undefined
114
- if (!input.is_tree_carried) return undefined
113
+ function hook_reuse(input: HookReuse): GateReuse {
114
+ if (input.extra_arguments.length > 0) return { miss: 'arguments were passed to the hook' }
115
+ if (is_force_requested(input.force_env)) return { miss: `${input.force_env} is set` }
116
+ if (!input.is_tree_carried) return { miss: input.carry_miss }
115
117
 
116
- return gate_skip.reusable_green_gate(input.tree.files, input.tree.base, input.source)
118
+ return gate_skip.gate_reuse(input.tree.files, input.tree.base, input.source)
117
119
  }
118
120
 
119
121
  const hook_gate_reuse = {
120
122
  GATE_REUSING_TARGETS,
123
+ hook_reuse,
121
124
  is_force_requested,
122
125
  is_index_matching_worktree,
123
126
  is_worktree_clean,
124
127
  read_status_lines,
125
- reusable_green_hook,
126
128
  }
127
129
 
128
130
  export type { HookReuse }
@@ -96,14 +96,17 @@ async function reusable_green_commit(
96
96
  source?: string,
97
97
  start_directory: string = process.cwd(),
98
98
  ): Promise<FileMapStamp | undefined> {
99
- const reusable = hook_gate_reuse.reusable_green_hook({
99
+ const reuse = hook_gate_reuse.hook_reuse({
100
100
  tree,
101
101
  is_tree_carried: tree.is_index_matching_worktree,
102
+ carry_miss: 'the index differs from the working tree',
102
103
  extra_arguments,
103
104
  force_env: FORCE_ENV,
104
105
  source,
105
106
  })
106
107
 
108
+ const reusable = gate_skip.stamp_of(reuse)
109
+
107
110
  if (reusable === undefined) return undefined
108
111
 
109
112
  return (await is_recorded_check_this_check(start_directory)) ? reusable : undefined