@joshuafolkken/kit 1.1178.0 → 1.1240.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 (442) 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 +7 -3
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +23 -2
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +9 -4
  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 +10 -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 +2 -1
  20. package/.claude/skills/workflow-commands/latest-gate.md +1 -1
  21. package/.claude/skills/workflow-commands/observation-ledger.md +3 -3
  22. package/.claude/skills/workflow-commands/pre-gate-cut.md +6 -1
  23. package/.claude/skills/workflow-commands/prerequisite.md +2 -2
  24. package/.claude/skills/workflow-commands/progress-watcher.md +1 -1
  25. package/.claude/skills/workflow-commands/working-tree-hold.md +1 -1
  26. package/.codex/hooks.json +2 -2
  27. package/.cursorrules +6 -41
  28. package/.github/actions/setup-node/action.yml +1 -1
  29. package/.github/workflows/auto-tag.yml +3 -4
  30. package/.github/workflows/ci.yml +32 -31
  31. package/.github/workflows/dependabot-auto-merge.yml +13 -6
  32. package/.github/workflows/publish.yml +50 -4
  33. package/.github/workflows/sonar-qube.yml +3 -0
  34. package/.pnpmfile.mjs +4 -3
  35. package/.prettierignore +8 -2
  36. package/CLAUDE.md +18 -20
  37. package/README.md +44 -38
  38. package/SECURITY.md +15 -1
  39. package/dist/CLAUDE.md +18 -20
  40. package/dist/commands/document-section-cli.js +734 -0
  41. package/dist/hooks/audit-provision.js +346 -0
  42. package/dist/hooks/batch-guard.js +5 -4
  43. package/dist/hooks/{chunk-G6KRCKEC.js → chunk-4TZVB44A.js} +496 -378
  44. package/dist/hooks/chunk-5VNDSNG3.js +12 -0
  45. package/dist/hooks/chunk-CDY4NFQG.js +15 -0
  46. package/dist/hooks/chunk-ELCN5LCH.js +135 -0
  47. package/dist/hooks/{chunk-VQLLNFNT.js → chunk-F3ELQ2AF.js} +3 -3
  48. package/dist/hooks/{chunk-I4XSL4TT.js → chunk-GPTFMK5E.js} +1 -1
  49. package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
  50. package/dist/hooks/{chunk-UAGLLIGZ.js → chunk-KHGB5S3Y.js} +7 -6
  51. package/dist/hooks/{chunk-GDS5N2KB.js → chunk-KHX4U3BG.js} +735 -683
  52. package/dist/hooks/{chunk-VESKRQOF.js → chunk-KRBERBBI.js} +3 -3
  53. package/dist/hooks/{chunk-DO6NWWHU.js → chunk-QVSWU24O.js} +4 -4
  54. package/dist/hooks/{chunk-S6X6WYKR.js → chunk-WK2XSHSI.js} +1643 -923
  55. package/dist/hooks/{chunk-ZAL3NRGL.js → chunk-XZYKEM6J.js} +25 -10
  56. package/dist/hooks/{chunk-32GQTL4F.js → chunk-ZQGD4GBY.js} +4 -2
  57. package/dist/hooks/codex-hook-adapter.js +14 -12
  58. package/dist/hooks/duplicate-read-guard.js +6 -5
  59. package/dist/hooks/format-edited.js +4 -3
  60. package/dist/hooks/inputs.json +203 -179
  61. package/dist/hooks/investigation-guard.js +6 -5
  62. package/dist/hooks/pretool-guard.js +12 -10
  63. package/dist/hooks/session-lang.js +2 -4
  64. package/dist/hooks/stop-guard.js +66 -72
  65. package/dist/josh.js +1088 -78
  66. package/dist/version/index.d.ts +7 -2
  67. package/dist/version/index.js +152 -75
  68. package/eslint/base.js +23 -1
  69. package/eslint/rules/code-quality.js +3 -0
  70. package/eslint/rules/early-return-one-liner.js +98 -0
  71. package/eslint/rules/unicorn.js +16 -1
  72. package/package.json +7 -6
  73. package/pnpm-workspace.yaml +9 -4
  74. package/prettier/basic.d.ts +3 -1
  75. package/prettier/index.d.ts +3 -1
  76. package/prettier/static.d.ts +3 -1
  77. package/prompts/coding-standards.md +1 -1
  78. package/prompts/collaboration-workflow/file-edits.md +4 -4
  79. package/prompts/collaboration-workflow/glossary.md +1 -1
  80. package/prompts/collaboration-workflow/issue-template.md +1 -0
  81. package/prompts/collaboration-workflow/operating-rules.md +17 -9
  82. package/prompts/collaboration-workflow/overview.md +2 -2
  83. package/prompts/collaboration-workflow/principles.md +14 -12
  84. package/prompts/collaboration-workflow/report-format.md +1 -1
  85. package/prompts/collaboration-workflow/residency.md +1 -1
  86. package/prompts/collaboration-workflow/rule-delivery.md +6 -6
  87. package/prompts/collaboration-workflow/shell-body.md +6 -6
  88. package/prompts/collaboration-workflow/turn-batching.md +1 -1
  89. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -2
  90. package/prompts/collaboration-workflow/wip-cap.md +10 -10
  91. package/prompts/review-rubric.md +1 -2
  92. package/prompts/review.md +1 -1
  93. package/scripts/agent/agent-headless.ts +29 -0
  94. package/scripts/agent/agent-launch-environment.ts +6 -2
  95. package/scripts/agent/codex-agent-argv.ts +2 -1
  96. package/scripts/auto-ok/auto-ok-cli.ts +1 -1
  97. package/scripts/backlog/backlog-arrival.ts +3 -3
  98. package/scripts/backlog/backlog-budget.ts +1 -1
  99. package/scripts/backlog/backlog-drive-args.ts +135 -0
  100. package/scripts/backlog/backlog-drive-cli.ts +11 -137
  101. package/scripts/backlog/backlog-drive-launch.ts +39 -0
  102. package/scripts/backlog/backlog-drive-named-offer.ts +3 -5
  103. package/scripts/backlog/backlog-drive-named.ts +3 -3
  104. package/scripts/backlog/backlog-drive-offer-argv.ts +1 -1
  105. package/scripts/backlog/backlog-drive-owner.ts +1 -1
  106. package/scripts/backlog/backlog-drive-restore.ts +1 -1
  107. package/scripts/backlog/backlog-drive-retrospective.ts +1 -1
  108. package/scripts/backlog/backlog-drive.ts +1 -1
  109. package/scripts/backlog/backlog-next.ts +16 -5
  110. package/scripts/backlog/backlog-offer-cli.ts +6 -6
  111. package/scripts/backlog/backlog-offer.ts +1 -1
  112. package/scripts/backlog/backlog-plan-cli.ts +1 -1
  113. package/scripts/backlog/backlog-pool.ts +57 -11
  114. package/scripts/backlog/backlog-ready.ts +1 -1
  115. package/scripts/backlog/backlog-scope.ts +1 -1
  116. package/scripts/backlog/backlog-stalled-detect.ts +2 -2
  117. package/scripts/backlog/backlog-stalled.ts +1 -1
  118. package/scripts/build/build-codex-hooks.ts +3 -1
  119. package/scripts/build/build-commands.ts +66 -0
  120. package/scripts/build/build-hooks.ts +2 -1
  121. package/scripts/build/build-library.ts +12 -3
  122. package/scripts/build/build.ts +70 -0
  123. package/scripts/build/local-bin.ts +3 -13
  124. package/scripts/delegation/investigation-reads.ts +3 -1
  125. package/scripts/discovery/repo-origin.ts +5 -5
  126. package/scripts/doctor/doctor-consumer.ts +2 -2
  127. package/scripts/doctor/doctor-io.ts +2 -3
  128. package/scripts/document/capped-print-part.ts +30 -0
  129. package/scripts/document/capped-print.ts +3 -4
  130. package/scripts/document/document-byte-budget.ts +11 -2
  131. package/scripts/document/document-scan.ts +117 -0
  132. package/scripts/document/document-section.ts +143 -16
  133. package/scripts/document/entry-read-budget.ts +5 -3
  134. package/scripts/document/entry-read-set.ts +10 -89
  135. package/scripts/document/read-set-cost.ts +98 -0
  136. package/scripts/epic/epic-add-body.ts +1 -0
  137. package/scripts/epic/epic-audit-checks.ts +1 -1
  138. package/scripts/epic/epic-audit-rationale.ts +4 -3
  139. package/scripts/epic/epic-bundle-cli.ts +14 -3
  140. package/scripts/epic/epic-bundle-poll.ts +41 -0
  141. package/scripts/epic/epic-busy.ts +1 -1
  142. package/scripts/epic/epic-candidate-confirm.ts +3 -2
  143. package/scripts/epic/epic-classify.ts +1 -1
  144. package/scripts/epic/epic-cli-argv.ts +78 -46
  145. package/scripts/epic/epic-cli-remove.ts +11 -19
  146. package/scripts/epic/epic-cli.ts +74 -73
  147. package/scripts/epic/epic-close-comment.ts +1 -1
  148. package/scripts/epic/epic-close.ts +1 -1
  149. package/scripts/epic/epic-cross-repo.ts +5 -5
  150. package/scripts/epic/epic-graph.ts +1 -1
  151. package/scripts/epic/epic-index.ts +1 -1
  152. package/scripts/epic/epic-issue.ts +2 -6
  153. package/scripts/epic/epic-lane-offer.ts +2 -2
  154. package/scripts/epic/epic-next.ts +29 -42
  155. package/scripts/epic/epic-solo-stale.ts +1 -1
  156. package/scripts/epic/epic-triage.ts +8 -4
  157. package/scripts/epic/epic-validate.ts +1 -1
  158. package/scripts/followup/git-followup-finish.ts +2 -2
  159. package/scripts/followup/git-followup-label.ts +2 -2
  160. package/scripts/followup/git-followup-merged.ts +2 -2
  161. package/scripts/followup/git-pr-followup-checks.ts +142 -0
  162. package/scripts/followup/git-pr-followup.ts +4 -129
  163. package/scripts/gate/core-admission.ts +162 -0
  164. package/scripts/gate/core-budget.ts +190 -68
  165. package/scripts/gate/gate-plan.ts +80 -7
  166. package/scripts/gate/gate-report.ts +28 -9
  167. package/scripts/gate/gate-skip.ts +38 -7
  168. package/scripts/gate/hook-gate-reuse.ts +12 -10
  169. package/scripts/gate/machine-capacity.ts +175 -0
  170. package/scripts/gate/pre-commit-type-check.ts +4 -1
  171. package/scripts/gate/project-checks.ts +6 -9
  172. package/scripts/gate/type-check-step.ts +2 -3
  173. package/scripts/gate/verification-gate.ts +34 -10
  174. package/scripts/gh/git-gh-api-path.ts +2 -2
  175. package/scripts/gh/git-gh-issue-rest.ts +3 -3
  176. package/scripts/gh/git-gh-issue-write.ts +53 -7
  177. package/scripts/gh/git-gh-pr-read.ts +64 -19
  178. package/scripts/gh/git-gh-pr-rest.ts +1 -1
  179. package/scripts/gh/git-gh-pr.ts +5 -1
  180. package/scripts/gh/git-pr-ai-review.ts +1 -1
  181. package/scripts/gh/git-pr-checks-eval.ts +1 -3
  182. package/scripts/gh/git-pr-checks-parse.ts +1 -1
  183. package/scripts/gh/git-pr-checks-watch.ts +6 -5
  184. package/scripts/gh/git-pr-checks.ts +7 -5
  185. package/scripts/gh/git-pr-coderabbit.ts +1 -1
  186. package/scripts/gh/git-pr-error.ts +1 -3
  187. package/scripts/gh/git-pr.ts +9 -13
  188. package/scripts/gh/github-issue-url.ts +2 -2
  189. package/scripts/git/constants.ts +2 -6
  190. package/scripts/git/git-command.ts +20 -218
  191. package/scripts/git/git-diff-reads.ts +225 -0
  192. package/scripts/git/git-error.ts +4 -12
  193. package/scripts/git/git-pre-push-hook.ts +139 -0
  194. package/scripts/git/git-prompt.ts +2 -6
  195. package/scripts/git/git-spawn-sync.ts +3 -1
  196. package/scripts/git/git-spawn.ts +1 -1
  197. package/scripts/git/git-staging.ts +4 -6
  198. package/scripts/git/git-status.ts +3 -9
  199. package/scripts/hooks/codex-hook-adapter.ts +1 -1
  200. package/scripts/hooks/edited-cspell.ts +2 -4
  201. package/scripts/hooks/format-edited-file.ts +3 -12
  202. package/scripts/hooks/pre-push-unit.ts +31 -9
  203. package/scripts/hooks/reserved-run.ts +2 -2
  204. package/scripts/hooks/run-hook.sh +5 -4
  205. package/scripts/hooks/stop-guard.ts +5 -3
  206. package/scripts/init/init-actions.ts +0 -5
  207. package/scripts/init/init-ai-copy.ts +13 -2
  208. package/scripts/init/init-logic-json-merge.ts +19 -0
  209. package/scripts/init/init-logic-secretlint.ts +7 -7
  210. package/scripts/init/init-logic-workspace.ts +41 -2
  211. package/scripts/init/init-logic.ts +16 -26
  212. package/scripts/init/init.ts +10 -16
  213. package/scripts/init/initial-commit.ts +4 -4
  214. package/scripts/init/kit-development-versions.ts +40 -0
  215. package/scripts/init/kit-setup-state.ts +2 -2
  216. package/scripts/init/package-manager-pin.ts +35 -0
  217. package/scripts/init/start-exec.ts +68 -19
  218. package/scripts/init/start-setup-pr.ts +9 -9
  219. package/scripts/init/start-steps.ts +6 -10
  220. package/scripts/issue/defect-rate-cli.ts +1 -1
  221. package/scripts/issue/git-next-issues.ts +1 -1
  222. package/scripts/issue/issue-auto-ok.ts +32 -7
  223. package/scripts/issue/issue-backlinks-cli.ts +2 -2
  224. package/scripts/issue/issue-backlinks.ts +9 -9
  225. package/scripts/issue/issue-comment-cli.ts +19 -13
  226. package/scripts/issue/issue-file-cli.ts +72 -22
  227. package/scripts/issue/issue-file.ts +29 -1
  228. package/scripts/issue/issue-labels.ts +13 -0
  229. package/scripts/issue/issue-release-cli.ts +155 -0
  230. package/scripts/issue/issue-release.ts +58 -0
  231. package/scripts/issue/issue-scout-closed.ts +1 -1
  232. package/scripts/issue/issue-state-cli.ts +30 -74
  233. package/scripts/josh/josh-command-map.ts +2 -0
  234. package/scripts/josh/josh-command-types.ts +14 -0
  235. package/scripts/josh/josh-commands-ai.ts +4 -4
  236. package/scripts/josh/josh-commands-backlog.ts +4 -1
  237. package/scripts/josh/josh-commands-development.ts +18 -0
  238. package/scripts/josh/josh-commands-document.ts +1 -0
  239. package/scripts/josh/josh-commands-issue.ts +8 -1
  240. package/scripts/josh/josh-commands-lane.ts +24 -1
  241. package/scripts/josh/josh-commands-maintenance.ts +6 -6
  242. package/scripts/josh/josh-commands-run.ts +12 -7
  243. package/scripts/josh/josh-in-process.ts +28 -9
  244. package/scripts/josh/josh-logic.ts +34 -15
  245. package/scripts/josh/process-identity.ts +1 -3
  246. package/scripts/josh/session-language-cli.ts +3 -4
  247. package/scripts/lane/lane-await.ts +5 -1
  248. package/scripts/lane/lane-child-invocation.ts +2 -2
  249. package/scripts/lane/lane-cli.ts +12 -4
  250. package/scripts/lane/lane-dispatch.ts +14 -10
  251. package/scripts/lane/lane-environment.ts +5 -1
  252. package/scripts/lane/lane-handoff.ts +1 -1
  253. package/scripts/lane/lane-install.ts +2 -13
  254. package/scripts/lane/lane-launch-cli.ts +15 -1
  255. package/scripts/lane/lane-ledger.ts +145 -0
  256. package/scripts/lane/lane-leftover.ts +44 -30
  257. package/scripts/lane/lane-load.ts +108 -0
  258. package/scripts/lane/lane-measure-cli.ts +149 -0
  259. package/scripts/lane/lane-occupancy.ts +3 -0
  260. package/scripts/lane/lane-open.ts +5 -5
  261. package/scripts/lane/lane-paths.ts +13 -0
  262. package/scripts/lane/lane-reap.ts +1 -3
  263. package/scripts/lane/lane-registry.ts +11 -0
  264. package/scripts/lane/lane-relaunch.ts +1 -1
  265. package/scripts/lane/lane-stats.ts +224 -0
  266. package/scripts/lane/lane-stray.ts +140 -0
  267. package/scripts/lane/lane-vacant.ts +36 -0
  268. package/scripts/lane/openai-lane-ship.ts +2 -2
  269. package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
  270. package/scripts/lane/openai-lane-supervisor.ts +7 -165
  271. package/scripts/lane/openai-review-broker.ts +2 -4
  272. package/scripts/lib/ancestor-directories.ts +35 -0
  273. package/scripts/lib/animation-helpers.ts +2 -2
  274. package/scripts/lib/buffered-process.ts +3 -7
  275. package/scripts/lib/cli-flags.ts +71 -1
  276. package/scripts/lib/spawn-exit.ts +2 -2
  277. package/scripts/lib/timeouts.ts +35 -2
  278. package/scripts/lines/line-budget.ts +4 -6
  279. package/scripts/notify/telegram-test.ts +6 -0
  280. package/scripts/observations/observation-ledger-line.ts +6 -6
  281. package/scripts/observations/observation-ledger-migrate.ts +55 -4
  282. package/scripts/observations/observation-ledger.ts +26 -7
  283. package/scripts/overrides/lockfile-overrides.ts +1 -1
  284. package/scripts/overrides/overrides-logic.ts +1 -1
  285. package/scripts/propagate/propagate-git.ts +1 -1
  286. package/scripts/propagate/propagate-publish.ts +2 -2
  287. package/scripts/propagate/propagate-steps.ts +20 -15
  288. package/scripts/propagate/propagate.ts +36 -27
  289. package/scripts/refactor/import-graph.ts +18 -1
  290. package/scripts/refactor/refactor-lint.ts +4 -4
  291. package/scripts/release/github-release-environment.ts +1 -0
  292. package/scripts/release/github-release-runs.ts +115 -0
  293. package/scripts/release/github-release.ts +13 -67
  294. package/scripts/repo/repository-labels.ts +2 -2
  295. package/scripts/repo/required-checks-logic.ts +1 -3
  296. package/scripts/review/live-evidence.ts +2 -4
  297. package/scripts/review/review-brief-cli.ts +1 -1
  298. package/scripts/review/review-finding-ledger.ts +1 -1
  299. package/scripts/review/review-findings-cli.ts +1 -3
  300. package/scripts/rules/decision-oracle-batch.ts +128 -0
  301. package/scripts/rules/decision-oracle-tokens.ts +21 -0
  302. package/scripts/rules/decision-oracle.ts +8 -120
  303. package/scripts/rules/delivered-rules.ts +26 -182
  304. package/scripts/rules/direct-filing.ts +1 -1
  305. package/scripts/rules/early-heartbeat.ts +3 -3
  306. package/scripts/rules/git-argv.ts +1 -1
  307. package/scripts/rules/git-force.ts +1 -1
  308. package/scripts/rules/implementation-cut.ts +2 -2
  309. package/scripts/rules/index-guard.ts +1 -1
  310. package/scripts/rules/issue-comments.ts +118 -0
  311. package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
  312. package/scripts/rules/lane-background.ts +2 -2
  313. package/scripts/rules/lane-park.ts +18 -0
  314. package/scripts/rules/lane-split-park.ts +128 -0
  315. package/scripts/rules/oracle-firing.ts +1 -1
  316. package/scripts/rules/poll-loop.ts +1 -1
  317. package/scripts/rules/pre-gate-cut.ts +1 -1
  318. package/scripts/rules/protected-files.ts +59 -4
  319. package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
  320. package/scripts/rules/shell-body-trigger.ts +1 -1
  321. package/scripts/rules/stop-rules.ts +18 -7
  322. package/scripts/rules/test-declared-commit.ts +4 -4
  323. package/scripts/rules/wip-cap.ts +63 -0
  324. package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
  325. package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
  326. package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
  327. package/scripts/run/{run-carry.ts → carry/run-carry.ts} +1 -1
  328. package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
  329. package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +18 -5
  330. package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
  331. package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
  332. package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
  333. package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +1 -0
  334. package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
  335. package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
  336. package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +20 -2
  337. package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
  338. package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
  339. package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
  340. package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
  341. package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
  342. package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
  343. package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
  344. package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
  345. package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
  346. package/scripts/run/run-ending.ts +2 -2
  347. package/scripts/run/run-halfrun-resume.ts +6 -1
  348. package/scripts/run/run-headless.ts +1 -1
  349. package/scripts/run/run-invocation.ts +85 -84
  350. package/scripts/run/run-liveness.ts +1 -1
  351. package/scripts/run/run-parent-cut-hook.ts +1 -1
  352. package/scripts/run/run-preflight.ts +6 -6
  353. package/scripts/run/run-prep-cli.ts +1 -1
  354. package/scripts/run/run-prrun-resume.ts +1 -1
  355. package/scripts/run/run-report-cli.ts +4 -4
  356. package/scripts/run/run-report.ts +2 -2
  357. package/scripts/run/run-status-cli.ts +1 -1
  358. package/scripts/run/run-step-cli.ts +3 -3
  359. package/scripts/run/run-step.ts +7 -4
  360. package/scripts/run/run-stop-notify.ts +1 -1
  361. package/scripts/run/run-stranded-detect.ts +4 -4
  362. package/scripts/run/run-stranded.ts +2 -2
  363. package/scripts/run/run-watcher-guard-cli.ts +1 -1
  364. package/scripts/run/run-watcher-guard.ts +5 -3
  365. package/scripts/run/run-watcher-hook.ts +1 -1
  366. package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +2 -2
  367. package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
  368. package/scripts/run/ship/run-ship-lock.ts +50 -0
  369. package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
  370. package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
  371. package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +4 -3
  372. package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
  373. package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
  374. package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +11 -12
  375. package/scripts/run/ship/run-ship-stop-text.ts +13 -0
  376. package/scripts/run/{run-ship-sync.ts → ship/run-ship-sync.ts} +25 -11
  377. package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
  378. package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +25 -11
  379. package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +58 -7
  380. package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +59 -8
  381. package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
  382. package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +13 -15
  383. package/scripts/run/wake/run-wake-restart.ts +58 -0
  384. package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
  385. package/scripts/run/{run-wake.ts → wake/run-wake.ts} +16 -65
  386. package/scripts/safe-chain/preinstall-command.ts +20 -21
  387. package/scripts/safe-chain/project-config.ts +19 -15
  388. package/scripts/sync/sync-ai-files.ts +1 -1
  389. package/scripts/sync/sync-configs.ts +2 -1
  390. package/scripts/sync/sync.ts +15 -2
  391. package/scripts/test/unit-worker-share.ts +1 -0
  392. package/scripts/time-runtime/time-background.ts +5 -1
  393. package/scripts/time-runtime/time-instant.ts +3 -1
  394. package/scripts/time-runtime/time-spans.ts +6 -139
  395. package/scripts/time-runtime/time-tool-call.ts +160 -0
  396. package/scripts/version/effective-upstream.ts +2 -20
  397. package/scripts/version/latest-corepack.ts +124 -93
  398. package/scripts/version/latest-regression.ts +1 -1
  399. package/scripts/version/latest-update.ts +4 -4
  400. package/scripts/version/npm-registry.ts +20 -5
  401. package/scripts/version/publishable-range-check.ts +2 -2
  402. package/scripts/version/publishable-range.ts +1 -1
  403. package/scripts/version/release-age.ts +48 -38
  404. package/scripts/version/release-hold.ts +1 -1
  405. package/scripts/version/safe-chain-version-update.ts +40 -0
  406. package/scripts/version/upgrade-shell-command.ts +48 -8
  407. package/scripts/version/version-check-logic.ts +23 -14
  408. package/scripts/version/version-commands.ts +23 -11
  409. package/scripts/version/version-remote.ts +28 -13
  410. package/templates/pnpm-workspace.basic.yaml +4 -0
  411. package/templates/workflows/dependabot-auto-merge.yml +5 -5
  412. package/templates/workflows/github-release.yml +6 -0
  413. package/scripts/build/build-config-merge.ts +0 -27
  414. package/scripts/build/build-managed-marker.ts +0 -27
  415. package/scripts/build/build-self-sync-guard.ts +0 -27
  416. package/scripts/build/build-version.ts +0 -26
  417. package/scripts/version/preinstall-version-update.ts +0 -86
  418. /package/scripts/git/{schemas.ts → git-schemas.ts} +0 -0
  419. /package/scripts/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
  420. /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
  421. /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
  422. /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
  423. /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
  424. /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
  425. /package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +0 -0
  426. /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
  427. /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
  428. /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
  429. /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
  430. /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
  431. /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
  432. /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
  433. /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
  434. /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
  435. /package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +0 -0
  436. /package/scripts/run/{run-ship-pre-detach.ts → ship/run-ship-pre-detach.ts} +0 -0
  437. /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
  438. /package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +0 -0
  439. /package/scripts/run/{run-ship.ts → ship/run-ship.ts} +0 -0
  440. /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
  441. /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
  442. /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
@@ -0,0 +1,162 @@
1
+ import type { LedgerLoad, MachineBudget } from './machine-capacity'
2
+
3
+ // The ledger's admission arithmetic, kept apart from the markers `core-budget.ts` reads and writes so
4
+ // that file stays under its line ceiling as the budget grew a memory side (joshuafolkken/kit#3371).
5
+ // Everything here is a pure function of the ledger it is handed.
6
+
7
+ const MS_PER_SECOND = 1000
8
+ // **How long an admitted claim's tool takes to reach its declared peak.** The machine reading holds only
9
+ // what a tool has already allocated, so a claim admitted moments ago is not in it yet: adding its
10
+ // declared memory back would admit the next claim into memory about to be taken — the concurrent start
11
+ // this ledger exists to stop. Until the window passes, the claim's memory counts only in the FIFO walk; a
12
+ // tool partway up is then counted twice, which costs a slower admission, never a swap. Its cores are in
13
+ // the reading from the start, so the window does not apply to them.
14
+ const RAMP_UP_MS = 30_000
15
+
16
+ // What admission reads from a marker. `memory_mb` and `admitted_at` are optional because a marker an
17
+ // older `josh` wrote has neither: it declares no memory, and it counts as load from outside the ledger.
18
+ interface LedgerReservation {
19
+ weight: number
20
+ claimed_at: number
21
+ memory_mb?: number | undefined
22
+ admitted_at?: number | undefined
23
+ }
24
+
25
+ interface LedgerEntry {
26
+ // The marker's filename, which is its identity in the ledger and the tiebreak when two reservations
27
+ // share a claim time.
28
+ key: string
29
+ reservation: LedgerReservation
30
+ }
31
+
32
+ // A claim admitted at the wait cap rather than by room in the budget — the run went ahead past the
33
+ // budget, and the gate's summary says so.
34
+ interface Overflow {
35
+ waited_ms: number
36
+ ledger: LedgerLoad
37
+ budget: MachineBudget
38
+ }
39
+
40
+ interface Admission {
41
+ overflow: Overflow | undefined
42
+ }
43
+
44
+ // Claim order, with the filename as the tiebreak so every reader agrees on the same ledger order
45
+ // however the wall clock rounded two near-simultaneous claims.
46
+ function compare_claim(left: LedgerEntry, right: LedgerEntry): number {
47
+ const by_time = left.reservation.claimed_at - right.reservation.claimed_at
48
+
49
+ return by_time === 0 ? left.key.localeCompare(right.key) : by_time
50
+ }
51
+
52
+ function add_load(totals: LedgerLoad, reservation: LedgerReservation): void {
53
+ totals.cores += reservation.weight
54
+ totals.memory_mb += reservation.memory_mb ?? 0
55
+ }
56
+
57
+ function exceeds(totals: LedgerLoad, budget: MachineBudget): boolean {
58
+ return totals.cores > budget.cores || totals.memory_mb > budget.memory_mb
59
+ }
60
+
61
+ // **The keys admitted under FIFO.** The head always runs — a single check heavier than the whole
62
+ // machine must not wait for room that will never come — and each later reservation runs while the
63
+ // cumulative weight ahead of it still fits the budget. The first that does not fit stops the walk, so a
64
+ // place is never jumped: admission is in claim order, which is what removes the start-time asymmetry.
65
+ // Memory is walked the same way, so a claim whose tool would not fit in free memory waits like one whose
66
+ // cores would not fit (joshuafolkken/kit#3371).
67
+ function admitted_keys(
68
+ reservations: ReadonlyArray<LedgerEntry>,
69
+ budget: number,
70
+ memory_budget = Infinity,
71
+ ): Set<string> {
72
+ const limit = { cores: budget, memory_mb: memory_budget }
73
+ const totals = { cores: 0, memory_mb: 0 }
74
+ const admitted = new Set<string>()
75
+
76
+ for (const [index, entry] of reservations.toSorted(compare_claim).entries()) {
77
+ add_load(totals, entry.reservation)
78
+
79
+ if (index !== 0 && exceeds(totals, limit)) break
80
+
81
+ admitted.add(entry.key)
82
+ }
83
+
84
+ return admitted
85
+ }
86
+
87
+ function is_admitted(
88
+ key: string,
89
+ reservations: ReadonlyArray<LedgerEntry>,
90
+ budget: MachineBudget,
91
+ ): boolean {
92
+ return admitted_keys(reservations, budget.cores, budget.memory_mb).has(key)
93
+ }
94
+
95
+ function sum_load(reservations: ReadonlyArray<LedgerEntry>): LedgerLoad {
96
+ const load = { cores: 0, memory_mb: 0 }
97
+
98
+ for (const entry of reservations) add_load(load, entry.reservation)
99
+
100
+ return load
101
+ }
102
+
103
+ function is_ramped(reservation: LedgerReservation, now: number): boolean {
104
+ const { admitted_at } = reservation
105
+
106
+ return admitted_at !== undefined && now - admitted_at >= RAMP_UP_MS
107
+ }
108
+
109
+ // **The load the ledger already accounts for.** CPU use shows in the reading the moment a tool starts,
110
+ // so every admitted claim's cores count at once; memory grows as the tool allocates, so only the claims
111
+ // admitted long enough ago to be in the reading count their memory.
112
+ function ledger_load(reservations: ReadonlyArray<LedgerEntry>, now: number): LedgerLoad {
113
+ const admitted = reservations.filter((entry) => entry.reservation.admitted_at !== undefined)
114
+ const ramped = admitted.filter((entry) => is_ramped(entry.reservation, now))
115
+
116
+ return { cores: sum_load(admitted).cores, memory_mb: sum_load(ramped).memory_mb }
117
+ }
118
+
119
+ function total_weight(reservations: ReadonlyArray<LedgerEntry>): number {
120
+ return sum_load(reservations).cores
121
+ }
122
+
123
+ // The weight actually running at once — what the reproduction test asserts stays within the budget.
124
+ function admitted_load(reservations: ReadonlyArray<LedgerEntry>, budget: number): number {
125
+ const admitted = admitted_keys(reservations, budget)
126
+
127
+ return total_weight(reservations.filter((entry) => admitted.has(entry.key)))
128
+ }
129
+
130
+ // The memory side of an overflow, so a claim held back by memory is not reported as a core shortfall.
131
+ // A budget that read no memory set no memory limit, and says nothing.
132
+ function describe_memory(ledger: LedgerLoad, budget: MachineBudget): string {
133
+ if (!Number.isFinite(budget.memory_mb)) return ''
134
+
135
+ return ` and ${String(Math.round(ledger.memory_mb))} MB of memory against ${String(Math.round(budget.memory_mb))} MB`
136
+ }
137
+
138
+ // The summary line for a claim admitted past the budget, or undefined for one that fitted.
139
+ function describe_overflow(admission: Admission): string | undefined {
140
+ const { overflow } = admission
141
+
142
+ if (overflow === undefined) return undefined
143
+
144
+ const { ledger, budget } = overflow
145
+ const waited = String(Math.round(overflow.waited_ms / MS_PER_SECOND))
146
+ const cores = `the ledger held ${String(ledger.cores)} cores against a budget of ${String(budget.cores)}`
147
+
148
+ return `started past the core budget after ${waited}s — ${cores}${describe_memory(ledger, budget)}`
149
+ }
150
+
151
+ const core_admission = {
152
+ RAMP_UP_MS,
153
+ admitted_keys,
154
+ admitted_load,
155
+ describe_overflow,
156
+ is_admitted,
157
+ ledger_load,
158
+ sum_load,
159
+ }
160
+
161
+ export type { Admission, LedgerEntry, Overflow }
162
+ export { core_admission }
@@ -6,7 +6,10 @@ 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'
11
+ import { core_admission, type Admission, type LedgerEntry, type Overflow } from './core-admission'
12
+ import { machine_capacity, type MachineBudget, type MachineReading } from './machine-capacity'
10
13
 
11
14
  // A machine-wide weighted core budget with admission control (joshuafolkken/kit#2351).
12
15
  //
@@ -33,6 +36,11 @@ import { z } from 'zod'
33
36
  // killed outright leaves its marker behind, and a ledger that trusted the file alone would fill forever
34
37
  // and admit nothing. The pid-and-start-time pair a marker carries is the same identity the unit-run
35
38
  // marker keeps, swept on read the moment its process is gone.
39
+ //
40
+ // **The budget is what the machine has free, in cores and in memory** (joshuafolkken/kit#3371). The
41
+ // core count stays the ceiling, but load from outside the ledger is subtracted from it, and a claim
42
+ // waits while the memory its tool holds is not free — `machine-capacity.ts` reads both. A claim marks
43
+ // its marker admitted once it runs, so the reading's load is split into the ledger's and the rest.
36
44
 
37
45
  const RESERVED_PREFIX = 'josh-core-reserved-'
38
46
  const RESERVED_SUFFIX = '.json'
@@ -46,22 +54,60 @@ const WAIT_CAP_MS = 120_000
46
54
  // The head of the ledger is always admitted, so the smallest budget worth planning against is one core.
47
55
  const MIN_BUDGET = 1
48
56
 
57
+ // **The cores each heavy tool occupies, declared once for both places that reserve them**
58
+ // (joshuafolkken/kit#3345). `gate-plan.ts`'s checks and the `josh` commands that run the same tools
59
+ // directly read these, so a direct `josh lint` claims what the gate's lint claims. The lint, type check
60
+ // and spell check numbers are `gate-plan.ts`'s measured table, floored. The whole-tree eslint scan
61
+ // behind `josh lines` and `josh refactor:scan` runs in one process: measured cold on the 11-core
62
+ // machine on 2026-10-06, 113s of CPU over 98s of wall, so it gets one core.
63
+ const CORE_WEIGHTS = {
64
+ lint: 2,
65
+ type_check: 1,
66
+ spell_check: 1,
67
+ eslint_scan: 1,
68
+ } as const
69
+
70
+ // **The memory each heavy tool holds, declared beside its cores** (joshuafolkken/kit#3371): peak
71
+ // resident size in MB, rounded up. Measured with `/usr/bin/time -l` on the 11-core / 18 GB machine on
72
+ // 2026-10-07: the type check 1,244 MB, the spell check 330 MB, and one unit worker 319 MB over 54 test
73
+ // files. Typed eslint was 1,207 MB over one directory and was observed at 1.6–2.4 GB per whole-tree
74
+ // `eslint .` with three lanes running the same day, so lint and the whole-tree scan take that peak.
75
+ const MEMORY_MB = {
76
+ lint: 2400,
77
+ type_check: 1300,
78
+ spell_check: 350,
79
+ eslint_scan: 2400,
80
+ unit_worker: 350,
81
+ } as const
82
+
83
+ // **The environment mark that says a parent already holds this process's cores**
84
+ // (joshuafolkken/kit#3345). `josh gate` reserves per check and then spawns `pnpm josh lint`. Without
85
+ // the mark, that child's own dispatch would claim a second place for the cores its parent already
86
+ // holds. Every holder sets it, and every child inherits it through the environment and claims nothing.
87
+ const HELD_KEY = 'JOSH_CORE_RESERVED'
88
+ const HELD_VALUE = '1'
89
+
90
+ // `memory_mb` and `admitted_at` are optional so a marker an older `josh` wrote still reads: it holds no
91
+ // declared memory, and it counts as load from outside the ledger until it is gone.
49
92
  const reservation_schema = process_owner_schema.extend({
50
93
  weight: z.number(),
51
94
  claimed_at: z.number(),
95
+ memory_mb: z.number().optional(),
96
+ admitted_at: z.number().optional(),
52
97
  })
53
98
 
54
99
  type Reservation = z.infer<typeof reservation_schema>
55
100
 
56
- interface LiveReservation {
57
- // The marker's filename, which is its identity in the ledger and the tiebreak when two reservations
58
- // share a claim time.
59
- key: string
101
+ type ReservationFields = Pick<Reservation, 'weight' | 'claimed_at' | 'memory_mb' | 'admitted_at'>
102
+
103
+ interface LiveReservation extends LedgerEntry {
60
104
  reservation: Reservation
61
105
  }
62
106
 
63
107
  interface ReserveOptions {
64
108
  budget?: number
109
+ memory_mb?: number | undefined
110
+ read_machine?: () => Promise<MachineReading>
65
111
  wait_cap_ms?: number
66
112
  poll_interval_ms?: number
67
113
  directory?: string
@@ -71,6 +117,7 @@ interface ReserveOptions {
71
117
 
72
118
  interface Handle {
73
119
  target: string
120
+ admission: Admission
74
121
  }
75
122
 
76
123
  function marker_name(key: string): string {
@@ -120,10 +167,13 @@ function live_reservation(source: string): Reservation | undefined {
120
167
  }
121
168
 
122
169
  // An unreadable temp directory answers "no other reservation", the direction that admits rather than
123
- // stalls a run on a guess.
170
+ // stalls a run on a guess. The suffix leaves out `replace_stamp`'s in-flight temporary copy of a marker,
171
+ // which would otherwise count one place twice.
124
172
  function marker_files(directory: string): Array<string> {
125
173
  try {
126
- return readdirSync(directory).filter((name) => name.startsWith(RESERVED_PREFIX))
174
+ return readdirSync(directory).filter(
175
+ (name) => name.startsWith(RESERVED_PREFIX) && name.endsWith(RESERVED_SUFFIX),
176
+ )
127
177
  } catch {
128
178
  return []
129
179
  }
@@ -133,54 +183,10 @@ function live_reservations(directory: string = PLATFORM_TEMP_ROOT): Array<LiveRe
133
183
  return marker_files(directory).flatMap((name) => {
134
184
  const reservation = live_reservation(path.join(directory, name))
135
185
 
136
- return reservation === undefined ? [] : [{ key: name, reservation }]
186
+ return reservation === undefined ? [] : { key: name, reservation }
137
187
  })
138
188
  }
139
189
 
140
- // Claim order, with the filename as the tiebreak so every reader agrees on the same ledger order
141
- // however the wall clock rounded two near-simultaneous claims.
142
- function compare_claim(left: LiveReservation, right: LiveReservation): number {
143
- const by_time = left.reservation.claimed_at - right.reservation.claimed_at
144
-
145
- return by_time === 0 ? left.key.localeCompare(right.key) : by_time
146
- }
147
-
148
- // **The keys admitted under FIFO.** The head always runs — a single check heavier than the whole
149
- // machine must not wait for room that will never come — and each later reservation runs while the
150
- // cumulative weight ahead of it still fits the budget. The first that does not fit stops the walk, so a
151
- // place is never jumped: admission is in claim order, which is what removes the start-time asymmetry.
152
- function admitted_keys(reservations: ReadonlyArray<LiveReservation>, budget: number): Set<string> {
153
- const admitted = new Set<string>()
154
- let cumulative = 0
155
-
156
- for (const [index, entry] of reservations.toSorted(compare_claim).entries()) {
157
- cumulative += entry.reservation.weight
158
-
159
- if (index !== 0 && cumulative > budget) break
160
-
161
- admitted.add(entry.key)
162
- }
163
-
164
- return admitted
165
- }
166
-
167
- function is_admitted(
168
- key: string,
169
- reservations: ReadonlyArray<LiveReservation>,
170
- budget: number,
171
- ): boolean {
172
- return admitted_keys(reservations, budget).has(key)
173
- }
174
-
175
- // The weight actually running at once — what the reproduction test asserts stays within the budget.
176
- function admitted_load(reservations: ReadonlyArray<LiveReservation>, budget: number): number {
177
- const admitted = admitted_keys(reservations, budget)
178
-
179
- return reservations
180
- .filter((entry) => admitted.has(entry.key))
181
- .reduce((total, entry) => total + entry.reservation.weight, 0)
182
- }
183
-
184
190
  // `Math.max` propagates `NaN` rather than clamping it, so an unusable budget is caught before it could
185
191
  // make every comparison false and admit everything — the hole `gate-plan.ts` closes for the same reason.
186
192
  function normalize_budget(budget: number): number {
@@ -193,23 +199,30 @@ async function default_sleep(ms: number): Promise<void> {
193
199
  })
194
200
  }
195
201
 
196
- function write_reservation(target: string, weight: number, claimed_at: number): void {
197
- stamp_file.write_stamp(target, { ...process_identity.own_fields(), weight, claimed_at })
202
+ // **Replaced atomically, never unlinked and recreated**: the admitted rewrite lands on a marker other
203
+ // lanes are polling, and a reader that hit the gap between an unlink and its create would see the ledger
204
+ // without this place and admit past it.
205
+ function write_reservation(target: string, fields: ReservationFields): void {
206
+ stamp_file.replace_stamp(target, { ...process_identity.own_fields(), ...fields })
198
207
  }
199
208
 
200
209
  // The clock and cadence a wait runs on, resolved apart from the ledger fields so `reserve` stays under
201
- // the complexity limit — each default is a branch, and six in one function is past it.
210
+ // the complexity limit — each default is a branch.
202
211
  interface TimingConfig {
203
212
  now: () => number
204
213
  sleep: (ms: number) => Promise<void>
205
214
  poll_interval_ms: number
215
+ wait_cap_ms: number
206
216
  }
207
217
 
208
218
  interface WaitContext extends TimingConfig {
219
+ read_machine: () => Promise<MachineReading>
209
220
  key: string
221
+ target: string
210
222
  directory: string
211
223
  budget: number
212
224
  deadline: number
225
+ fields: ReservationFields
213
226
  }
214
227
 
215
228
  function resolve_timing(options: ReserveOptions): TimingConfig {
@@ -217,39 +230,96 @@ function resolve_timing(options: ReserveOptions): TimingConfig {
217
230
  now: options.now ?? Date.now,
218
231
  sleep: options.sleep ?? default_sleep,
219
232
  poll_interval_ms: options.poll_interval_ms ?? POLL_INTERVAL_MS,
233
+ wait_cap_ms: options.wait_cap_ms ?? WAIT_CAP_MS,
234
+ }
235
+ }
236
+
237
+ // **A claim alone in the ledger reads nothing from the machine**: the head is admitted whatever the
238
+ // budget says, so a solo run pays no sampling window and starts exactly as it did before.
239
+ async function current_budget(
240
+ context: WaitContext,
241
+ reservations: ReadonlyArray<LiveReservation>,
242
+ ): Promise<MachineBudget> {
243
+ if (reservations.length <= 1) return { cores: context.budget, memory_mb: Infinity }
244
+
245
+ const reading = await context.read_machine()
246
+ const ledger = core_admission.ledger_load(reservations, context.now())
247
+ const budget = machine_capacity.machine_budget(context.budget, reading, ledger)
248
+
249
+ return { ...budget, cores: normalize_budget(budget.cores) }
250
+ }
251
+
252
+ // The claim is admitted: its marker records when, which moves its load from "load outside the ledger"
253
+ // to "load the ledger holds" for every later reader — its cores at once, its memory once its tool has
254
+ // had time to ramp up.
255
+ function admit(context: WaitContext, overflow?: Overflow): Admission {
256
+ write_reservation(context.target, { ...context.fields, admitted_at: context.now() })
257
+
258
+ return { overflow }
259
+ }
260
+
261
+ function overflow_of(
262
+ context: WaitContext,
263
+ reservations: ReadonlyArray<LiveReservation>,
264
+ budget: MachineBudget,
265
+ ): Overflow {
266
+ return {
267
+ waited_ms: context.now() - context.fields.claimed_at,
268
+ ledger: core_admission.sum_load(reservations),
269
+ budget,
220
270
  }
221
271
  }
222
272
 
273
+ async function poll_admission(context: WaitContext): Promise<Admission | undefined> {
274
+ const reservations = live_reservations(context.directory)
275
+ const budget = await current_budget(context, reservations)
276
+
277
+ if (core_admission.is_admitted(context.key, reservations, budget)) return admit(context)
278
+
279
+ if (context.now() >= context.deadline) {
280
+ return admit(context, overflow_of(context, reservations, budget))
281
+ }
282
+
283
+ return undefined
284
+ }
285
+
223
286
  // Poll the ledger until this reservation is admitted, or until the wait cap admits it at minimum width.
224
287
  // A solo run is admitted on the first read — its own claim is the ledger's head — so the loop body never
225
288
  // runs and the wait costs nothing.
226
- async function await_admission(context: WaitContext): Promise<void> {
227
- while (!is_admitted(context.key, live_reservations(context.directory), context.budget)) {
228
- if (context.now() >= context.deadline) return
289
+ async function await_admission(context: WaitContext): Promise<Admission> {
290
+ let admission = await poll_admission(context)
229
291
 
292
+ while (admission === undefined) {
230
293
  // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
231
294
  await context.sleep(context.poll_interval_ms)
295
+ // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
296
+ admission = await poll_admission(context)
232
297
  }
298
+
299
+ return admission
233
300
  }
234
301
 
235
302
  async function reserve(weight: number, options: ReserveOptions = {}): Promise<Handle> {
236
303
  const timing = resolve_timing(options)
237
304
  const directory = options.directory ?? PLATFORM_TEMP_ROOT
238
- const budget = normalize_budget(options.budget ?? availableParallelism())
239
305
  const claimed_at = timing.now()
240
306
  const name = marker_name(randomUUID())
241
307
  const target = path.join(directory, name)
308
+ const fields = { weight, claimed_at, memory_mb: options.memory_mb }
242
309
 
243
- write_reservation(target, weight, claimed_at)
244
- await await_admission({
310
+ write_reservation(target, fields)
311
+ const admission = await await_admission({
245
312
  ...timing,
246
313
  key: name,
314
+ target,
247
315
  directory,
248
- budget,
249
- deadline: claimed_at + (options.wait_cap_ms ?? WAIT_CAP_MS),
316
+ budget: normalize_budget(options.budget ?? availableParallelism()),
317
+ deadline: claimed_at + timing.wait_cap_ms,
318
+ fields,
319
+ read_machine: options.read_machine ?? machine_capacity.read_machine,
250
320
  })
251
321
 
252
- return { target }
322
+ return { target, admission }
253
323
  }
254
324
 
255
325
  function release(handle: Handle): void {
@@ -257,30 +327,82 @@ function release(handle: Handle): void {
257
327
  }
258
328
 
259
329
  // The whole reservation lifetime around a step: claim a place, wait for admission, run, and free the
260
- // place on any exit. `finally`, so a step that threw still releases its cores.
330
+ // place on any exit. `finally`, so a step that threw still releases its cores. The step is told how it was
331
+ // admitted, so the gate can report one that started past the budget.
261
332
  async function with_core_reservation<T>(
262
333
  weight: number,
263
- run: () => Promise<T>,
334
+ run: (admission: Admission) => Promise<T>,
264
335
  options: ReserveOptions = {},
265
336
  ): Promise<T> {
266
337
  const handle = await reserve(weight, options)
267
338
 
268
339
  try {
269
- return await run()
340
+ return await run(handle.admission)
270
341
  } finally {
271
342
  release(handle)
272
343
  }
273
344
  }
274
345
 
346
+ function is_held(): boolean {
347
+ return process.env[HELD_KEY] === HELD_VALUE
348
+ }
349
+
350
+ // A function of its own so the restoring write does not sit after an `await`, where
351
+ // `require-atomic-updates` reads any write to `process` as a possible race — the reason `josh.ts`
352
+ // gives `record_exit_code`.
353
+ function set_held_mark(value: string): void {
354
+ process.env[HELD_KEY] = value
355
+ }
356
+
357
+ // Marks this process, and every child it spawns, as running inside a held reservation. The previous
358
+ // value comes back afterwards, so a nested holder never clears the mark its outer holder set.
359
+ async function with_held_mark<T>(run: () => Promise<T>): Promise<T> {
360
+ const previous = process.env[HELD_KEY] ?? ''
361
+
362
+ set_held_mark(HELD_VALUE)
363
+
364
+ try {
365
+ return await run()
366
+ } finally {
367
+ set_held_mark(previous)
368
+ }
369
+ }
370
+
371
+ // **A command's reservation: the gate's claim, unless a parent already holds the cores**
372
+ // (joshuafolkken/kit#3345). `josh-logic.ts` calls this around every command that declares a weight.
373
+ // Each command then only declares its weight, and no command carries a copy of the claim, run and
374
+ // release steps. A solo run is admitted on the first read, so a run with room on the machine starts
375
+ // with no wait. A command inside a unit suite claims nothing either, for the reason `reserved-run.ts`
376
+ // gives: the suite's own reservation already covers it, and a place claimed there would wait on it.
377
+ async function with_command_reservation<T>(
378
+ weight: number,
379
+ run: () => Promise<T>,
380
+ options: ReserveOptions = {},
381
+ ): Promise<T> {
382
+ if (is_held() || unit_worker_share.is_nested_run()) return await run()
383
+
384
+ return await with_core_reservation(weight, async () => await with_held_mark(run), options)
385
+ }
386
+
275
387
  const core_budget = {
388
+ CORE_WEIGHTS,
389
+ HELD_KEY,
390
+ HELD_VALUE,
391
+ MEMORY_MB,
276
392
  RESERVED_PREFIX,
277
- admitted_keys,
278
- admitted_load,
393
+ admitted_keys: core_admission.admitted_keys,
394
+ admitted_load: core_admission.admitted_load,
395
+ describe_overflow: core_admission.describe_overflow,
396
+ is_held,
279
397
  live_reservations,
280
398
  release,
281
399
  reserve,
400
+ with_command_reservation,
282
401
  with_core_reservation,
402
+ with_held_mark,
283
403
  }
284
404
 
285
405
  export type { LiveReservation, Reservation, ReserveOptions }
286
406
  export { core_budget }
407
+
408
+ export { type Admission } from './core-admission'
@@ -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).
@@ -35,6 +36,9 @@ interface GateCheck {
35
36
  // The unit suite reserves nothing: it sizes its own pool from the machine, so it is capped
36
37
  // instead of reserved, and counting it here would reserve cores against itself.
37
38
  reserved_cores: number
39
+ // The memory this check holds while it runs, `core_budget.MEMORY_MB`'s measured peak; none declared is
40
+ // none waited for (joshuafolkken/kit#3371). The unit suite's is per worker, so it is derived instead.
41
+ memory_mb?: number
38
42
  }
39
43
 
40
44
  const LINT_LABEL = 'lint'
@@ -61,15 +65,41 @@ const WARNING_CHECKER_LABELS: ReadonlyArray<string> = [LINT_LABEL, TYPE_CHECK_LA
61
65
  // check rather than one appended after the unit suite is what keeps the plan's shape intact: the
62
66
  // `--no-unit` set stays "the whole gate minus the unit suite", and a CI runner — which has no recorded
63
67
  // transcripts — runs it as a trivial green rather than skipping it.
68
+ //
69
+ // **The three reserving weights are `core_budget.CORE_WEIGHTS`** (joshuafolkken/kit#3345), the same
70
+ // numbers the `josh lint`, `josh check` and `josh cspell:dot` commands claim when run directly.
71
+ //
72
+ // **The two zero weights were measured again for joshuafolkken/kit#3345, and they stay zero.** These are
73
+ // warm runs on the 11-core machine on 2026-10-06. `behavior` took 0.6s of wall for 0.7s of CPU, which
74
+ // is node starting up. `exports:unused` took 5.5s of wall for 9.0s of CPU, about 1.6 cores. Floored,
75
+ // that would be one core, but a reservation lasts the whole gate's plan, not the check's five seconds.
76
+ // It would lower `RESERVED_CORES` from 4 to 5 and cut the solo unit cap from 7 workers to 6 for the
77
+ // full length of the unit suite, to cover a burst that ends before lint does. Nothing has measured
78
+ // that trade, so the measured shape stays.
64
79
  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 },
80
+ {
81
+ label: LINT_LABEL,
82
+ target: 'lint',
83
+ reserved_cores: core_budget.CORE_WEIGHTS.lint,
84
+ memory_mb: core_budget.MEMORY_MB.lint,
85
+ },
86
+ {
87
+ label: TYPE_CHECK_LABEL,
88
+ target: 'check',
89
+ reserved_cores: core_budget.CORE_WEIGHTS.type_check,
90
+ memory_mb: core_budget.MEMORY_MB.type_check,
91
+ },
92
+ {
93
+ label: 'cspell',
94
+ target: 'cspell:dot',
95
+ reserved_cores: core_budget.CORE_WEIGHTS.spell_check,
96
+ memory_mb: core_budget.MEMORY_MB.spell_check,
97
+ },
68
98
  { label: BEHAVIOR_LABEL, target: BEHAVIOR_LABEL, reserved_cores: 0 },
69
99
  // **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.
100
+ // program and walks it for about 5s, and it is over well before lint and the type check. A
101
+ // reservation would only take a place from the four-core CI runner's fan-out and a worker from the
102
+ // unit suite for the whole gate.
73
103
  { label: 'exports', target: 'exports:unused', reserved_cores: 0 },
74
104
  ]
75
105
 
@@ -242,6 +272,11 @@ function resolve_unit_worker_cap(
242
272
  return available_cores - RESERVED_CORES
243
273
  }
244
274
 
275
+ // The unit suite's weight when nothing caps its pool: the cores left once the static checks have theirs.
276
+ function solo_unit_weight(available_cores: number): number {
277
+ return Math.max(0, available_cores - RESERVED_CORES)
278
+ }
279
+
245
280
  // The cores a check reserves from the machine-wide budget while it runs (joshuafolkken/kit#2351). The
246
281
  // static checks declare their measured reservation; the unit suite declares the pool it will open,
247
282
  // which `unit_worker_cap` has already sized against the runs in flight. **This is the same table, read
@@ -256,7 +291,42 @@ function resolve_unit_worker_cap(
256
291
  function check_weight(check: GateCheck, plan: GatePlan, available_cores: number): number {
257
292
  if (check.label !== UNIT_LABEL) return check.reserved_cores
258
293
 
259
- return plan.unit_worker_cap ?? Math.max(0, available_cores - RESERVED_CORES)
294
+ return plan.unit_worker_cap ?? solo_unit_weight(available_cores)
295
+ }
296
+
297
+ function unit_memory(workers: number): number {
298
+ return workers * core_budget.MEMORY_MB.unit_worker
299
+ }
300
+
301
+ // The memory a check holds (joshuafolkken/kit#3371): its declared figure, or for the unit suite one
302
+ // worker's peak for each core it reserves — its weight is its worker count.
303
+ function check_memory(check: GateCheck, plan: GatePlan, available_cores: number): number {
304
+ if (check.label !== UNIT_LABEL) return check.memory_mb ?? 0
305
+
306
+ return unit_memory(check_weight(check, plan, available_cores))
307
+ }
308
+
309
+ // The cores a unit run started directly reserves (joshuafolkken/kit#3345): the share it will pass to
310
+ // vitest while other runs are live, and otherwise the gate's own solo unit weight. **Not the whole
311
+ // machine**, though a lone vitest sizes its own pool: FIFO admission lets a claim of every core in only
312
+ // once the ledger is empty, so a one-file `josh test:related` would wait out every other lane's gate
313
+ // and hold every later claim behind it. One weight for the suite however it is started. **Floored at one
314
+ // worker**: on a machine the static checks fill, a four-core CI runner among them, the gate's solo
315
+ // weight is zero because its siblings hold every core — a direct run has no siblings, and vitest still
316
+ // occupies a core.
317
+ function direct_unit_weight(
318
+ available_cores: number = availableParallelism(),
319
+ live_runs: number = unit_worker_share.live_run_count(),
320
+ ): number {
321
+ const weight =
322
+ unit_worker_share.current_share(available_cores, live_runs) ?? solo_unit_weight(available_cores)
323
+
324
+ return Math.max(unit_worker_share.MIN_WORKERS, weight)
325
+ }
326
+
327
+ // What a unit run started directly holds: one worker's peak for each worker `direct_unit_weight` gives it.
328
+ function direct_unit_memory(): number {
329
+ return unit_memory(direct_unit_weight())
260
330
  }
261
331
 
262
332
  // `availableParallelism()` rather than `cpus().length`: it reports what this process may actually
@@ -344,7 +414,10 @@ const gate_plan = {
344
414
  TYPE_CHECK_LABEL,
345
415
  UNIT_LABEL,
346
416
  WARNING_CHECKER_LABELS,
417
+ check_memory,
347
418
  check_weight,
419
+ direct_unit_memory,
420
+ direct_unit_weight,
348
421
  format_gate_plan,
349
422
  format_machine,
350
423
  has_unit_check,