@joshuafolkken/kit 1.1166.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 (437) 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 +9 -5
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +29 -9
  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 +24 -15
  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 +10 -5
  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 +47 -33
  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-DYAVAM6S.js → chunk-55HHG5PX.js} +1405 -631
  43. package/dist/hooks/{chunk-A6P7PN7P.js → chunk-5VMKCCC4.js} +25 -10
  44. package/dist/hooks/chunk-5VNDSNG3.js +12 -0
  45. package/dist/hooks/{chunk-5KTCMCOT.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-ENA7P5FB.js → chunk-F56MFS4B.js} +3 -3
  49. package/dist/hooks/{chunk-ANJ2WGFD.js → chunk-FIVDHQGT.js} +7 -6
  50. package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
  51. package/dist/hooks/{chunk-GU5MATVE.js → chunk-JUDKDCDP.js} +499 -389
  52. package/dist/hooks/{chunk-ZDZ555YH.js → chunk-NV2JRZZO.js} +743 -909
  53. package/dist/hooks/{chunk-V2BTOZHT.js → chunk-RNWHN5BP.js} +3 -3
  54. package/dist/hooks/{chunk-URJA6XY4.js → chunk-TQNPDZNH.js} +4 -4
  55. package/dist/hooks/{chunk-GYNGP7HI.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 +232 -210
  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 +81 -59
  64. package/dist/josh.js +872 -75
  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 +31 -9
  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 +7 -6
  111. package/scripts/backlog/backlog-pool.ts +57 -11
  112. package/scripts/backlog/backlog-rank.ts +18 -9
  113. package/scripts/backlog/backlog-ready.ts +1 -1
  114. package/scripts/backlog/backlog-restructure.ts +152 -0
  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/backlog/backlog-waves.ts +15 -17
  119. package/scripts/build/build-codex-hooks.ts +3 -1
  120. package/scripts/build/build-commands.ts +66 -0
  121. package/scripts/build/build-hooks.ts +2 -1
  122. package/scripts/build/build-library.ts +12 -3
  123. package/scripts/build/build.ts +70 -0
  124. package/scripts/build/local-bin.ts +3 -13
  125. package/scripts/cost-runtime/context-cut-threshold.ts +1 -9
  126. package/scripts/cost-runtime/cost-verdict.ts +15 -28
  127. package/scripts/delegation/investigation-reads.ts +3 -1
  128. package/scripts/discovery/repo-origin.ts +5 -5
  129. package/scripts/doctor/doctor-consumer.ts +2 -2
  130. package/scripts/doctor/doctor-io.ts +2 -3
  131. package/scripts/document/capped-print-part.ts +30 -0
  132. package/scripts/document/capped-print.ts +3 -4
  133. package/scripts/document/document-byte-budget.ts +11 -2
  134. package/scripts/document/document-scan.ts +117 -0
  135. package/scripts/document/document-section.ts +143 -16
  136. package/scripts/document/entry-read-budget.ts +5 -3
  137. package/scripts/document/entry-read-set.ts +10 -89
  138. package/scripts/document/read-set-cost.ts +98 -0
  139. package/scripts/epic/epic-add-body.ts +1 -0
  140. package/scripts/epic/epic-audit-checks.ts +1 -1
  141. package/scripts/epic/epic-audit-rationale.ts +4 -3
  142. package/scripts/epic/epic-bundle-cli.ts +14 -3
  143. package/scripts/epic/epic-bundle-poll.ts +41 -0
  144. package/scripts/epic/epic-busy.ts +1 -1
  145. package/scripts/epic/epic-candidate-confirm.ts +3 -2
  146. package/scripts/epic/epic-classify.ts +1 -1
  147. package/scripts/epic/epic-cli-argv.ts +78 -46
  148. package/scripts/epic/epic-cli-remove.ts +11 -19
  149. package/scripts/epic/epic-cli.ts +74 -73
  150. package/scripts/epic/epic-close-comment.ts +1 -1
  151. package/scripts/epic/epic-close.ts +1 -1
  152. package/scripts/epic/epic-cross-repo.ts +5 -5
  153. package/scripts/epic/epic-graph.ts +1 -1
  154. package/scripts/epic/epic-index.ts +1 -1
  155. package/scripts/epic/epic-issue.ts +2 -6
  156. package/scripts/epic/epic-lane-offer.ts +2 -2
  157. package/scripts/epic/epic-next.ts +29 -42
  158. package/scripts/epic/epic-solo-stale.ts +1 -1
  159. package/scripts/epic/epic-triage.ts +8 -4
  160. package/scripts/epic/epic-validate.ts +1 -1
  161. package/scripts/followup/git-followup-finish.ts +2 -2
  162. package/scripts/followup/git-followup-label.ts +2 -2
  163. package/scripts/followup/git-followup-merged.ts +2 -2
  164. package/scripts/followup/git-followup-stages.ts +9 -0
  165. package/scripts/followup/git-pr-followup-checks.ts +142 -0
  166. package/scripts/followup/git-pr-followup.ts +5 -130
  167. package/scripts/gate/core-budget.ts +69 -1
  168. package/scripts/gate/gate-plan.ts +47 -7
  169. package/scripts/gate/gate-skip.ts +38 -7
  170. package/scripts/gate/hook-gate-reuse.ts +12 -10
  171. package/scripts/gate/pre-commit-type-check.ts +4 -1
  172. package/scripts/gate/project-checks.ts +6 -9
  173. package/scripts/gate/type-check-step.ts +2 -3
  174. package/scripts/gate/verification-gate.ts +27 -8
  175. package/scripts/gh/git-gh-api-path.ts +2 -2
  176. package/scripts/gh/git-gh-issue-rest.ts +3 -3
  177. package/scripts/gh/git-gh-issue-write.ts +53 -7
  178. package/scripts/gh/git-gh-issue.ts +5 -3
  179. package/scripts/gh/git-gh-pr-read.ts +68 -19
  180. package/scripts/gh/git-gh-pr-rest.ts +1 -1
  181. package/scripts/gh/git-gh-pr.ts +5 -1
  182. package/scripts/gh/git-pr-ai-review.ts +1 -1
  183. package/scripts/gh/git-pr-checks-eval.ts +9 -3
  184. package/scripts/gh/git-pr-checks-parse.ts +1 -1
  185. package/scripts/gh/git-pr-checks-watch.ts +6 -5
  186. package/scripts/gh/git-pr-checks.ts +18 -6
  187. package/scripts/gh/git-pr-coderabbit.ts +1 -1
  188. package/scripts/gh/git-pr-error.ts +1 -3
  189. package/scripts/gh/git-pr.ts +9 -13
  190. package/scripts/gh/github-issue-url.ts +2 -2
  191. package/scripts/git/constants.ts +12 -6
  192. package/scripts/git/git-command.ts +20 -218
  193. package/scripts/git/git-diff-reads.ts +225 -0
  194. package/scripts/git/git-error.ts +4 -12
  195. package/scripts/git/git-pre-push-hook.ts +139 -0
  196. package/scripts/git/git-prompt.ts +2 -6
  197. package/scripts/git/{schemas.ts → git-schemas.ts} +2 -0
  198. package/scripts/git/git-spawn-sync.ts +3 -1
  199. package/scripts/git/git-spawn.ts +1 -1
  200. package/scripts/git/git-staging.ts +4 -6
  201. package/scripts/git/git-status.ts +3 -9
  202. package/scripts/git/main-merge.ts +57 -21
  203. package/scripts/hooks/codex-hook-adapter.ts +1 -1
  204. package/scripts/hooks/edited-cspell.ts +2 -4
  205. package/scripts/hooks/format-edited-file.ts +3 -12
  206. package/scripts/hooks/pre-push-unit.ts +31 -9
  207. package/scripts/hooks/reserved-run.ts +2 -2
  208. package/scripts/hooks/run-hook.sh +5 -4
  209. package/scripts/hooks/stop-guard.ts +5 -3
  210. package/scripts/init/init-actions.ts +0 -5
  211. package/scripts/init/init-ai-copy.ts +13 -2
  212. package/scripts/init/init-logic-json-merge.ts +19 -0
  213. package/scripts/init/init-logic-secretlint.ts +7 -7
  214. package/scripts/init/init-logic-workspace.ts +41 -2
  215. package/scripts/init/init-logic.ts +16 -26
  216. package/scripts/init/init.ts +5 -15
  217. package/scripts/init/initial-commit.ts +4 -4
  218. package/scripts/init/kit-development-versions.ts +40 -0
  219. package/scripts/init/kit-setup-state.ts +2 -2
  220. package/scripts/init/start-exec.ts +68 -19
  221. package/scripts/init/start-setup-pr.ts +9 -9
  222. package/scripts/init/start-steps.ts +6 -10
  223. package/scripts/issue/defect-rate-cli.ts +1 -1
  224. package/scripts/issue/git-next-issues.ts +1 -1
  225. package/scripts/issue/issue-auto-ok.ts +1 -1
  226. package/scripts/issue/issue-backlinks-cli.ts +2 -2
  227. package/scripts/issue/issue-backlinks.ts +9 -9
  228. package/scripts/issue/issue-comment-cli.ts +19 -13
  229. package/scripts/issue/issue-file-cli.ts +45 -18
  230. package/scripts/issue/issue-file.ts +24 -1
  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 +11 -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 +11 -1
  238. package/scripts/josh/josh-commands-document.ts +1 -0
  239. package/scripts/josh/josh-commands-lane.ts +22 -0
  240. package/scripts/josh/josh-commands-maintenance.ts +4 -4
  241. package/scripts/josh/josh-commands-run.ts +12 -7
  242. package/scripts/josh/josh-in-process.ts +28 -9
  243. package/scripts/josh/josh-logic.ts +33 -15
  244. package/scripts/josh/process-identity.ts +1 -3
  245. package/scripts/josh/session-language-cli.ts +3 -4
  246. package/scripts/lane/lane-await.ts +5 -1
  247. package/scripts/lane/lane-child-invocation.ts +2 -2
  248. package/scripts/lane/lane-cli.ts +3 -3
  249. package/scripts/lane/lane-dispatch.ts +14 -10
  250. package/scripts/lane/lane-environment.ts +5 -1
  251. package/scripts/lane/lane-handoff.ts +1 -1
  252. package/scripts/lane/lane-install.ts +2 -13
  253. package/scripts/lane/lane-launch-cli.ts +35 -3
  254. package/scripts/lane/lane-ledger.ts +145 -0
  255. package/scripts/lane/lane-leftover.ts +1 -1
  256. package/scripts/lane/lane-load.ts +108 -0
  257. package/scripts/lane/lane-measure-cli.ts +149 -0
  258. package/scripts/lane/lane-occupancy.ts +3 -0
  259. package/scripts/lane/lane-reap.ts +1 -3
  260. package/scripts/lane/lane-relaunch.ts +1 -1
  261. package/scripts/lane/lane-stats.ts +224 -0
  262. package/scripts/lane/lane-vacant.ts +36 -0
  263. package/scripts/lane/openai-lane-ship.ts +2 -2
  264. package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
  265. package/scripts/lane/openai-lane-supervisor.ts +7 -165
  266. package/scripts/lane/openai-review-broker.ts +2 -4
  267. package/scripts/lib/ancestor-directories.ts +35 -0
  268. package/scripts/lib/animation-helpers.ts +2 -2
  269. package/scripts/lib/buffered-process.ts +3 -7
  270. package/scripts/lib/cli-flags.ts +71 -1
  271. package/scripts/lib/spawn-exit.ts +2 -2
  272. package/scripts/lib/timeouts.ts +35 -2
  273. package/scripts/lines/line-budget.ts +4 -6
  274. package/scripts/notify/telegram-notify.ts +26 -1
  275. package/scripts/notify/telegram-test.ts +6 -0
  276. package/scripts/observations/observation-ledger-line.ts +6 -6
  277. package/scripts/observations/observation-ledger-migrate.ts +55 -4
  278. package/scripts/observations/observation-ledger.ts +26 -7
  279. package/scripts/overrides/lockfile-overrides.ts +1 -1
  280. package/scripts/overrides/overrides-logic.ts +1 -1
  281. package/scripts/propagate/propagate-git.ts +1 -1
  282. package/scripts/propagate/propagate-publish.ts +2 -2
  283. package/scripts/propagate/propagate-steps.ts +20 -15
  284. package/scripts/propagate/propagate.ts +36 -27
  285. package/scripts/refactor/import-graph.ts +18 -1
  286. package/scripts/refactor/refactor-lint.ts +4 -4
  287. package/scripts/repo/repository-labels.ts +2 -2
  288. package/scripts/repo/required-checks-logic.ts +1 -3
  289. package/scripts/review/live-evidence.ts +2 -4
  290. package/scripts/review/review-brief-cli.ts +1 -1
  291. package/scripts/review/review-finding-ledger.ts +1 -1
  292. package/scripts/review/review-findings-cli.ts +1 -3
  293. package/scripts/rules/decision-oracle-batch.ts +128 -0
  294. package/scripts/rules/decision-oracle-tokens.ts +21 -0
  295. package/scripts/rules/decision-oracle.ts +8 -120
  296. package/scripts/rules/delivered-rules.ts +26 -182
  297. package/scripts/rules/direct-filing.ts +1 -1
  298. package/scripts/rules/early-heartbeat.ts +3 -3
  299. package/scripts/rules/git-argv.ts +1 -1
  300. package/scripts/rules/git-force.ts +1 -1
  301. package/scripts/rules/implementation-cut.ts +2 -2
  302. package/scripts/rules/index-guard.ts +1 -1
  303. package/scripts/rules/issue-comments.ts +118 -0
  304. package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
  305. package/scripts/rules/lane-background.ts +2 -2
  306. package/scripts/rules/lane-park.ts +18 -0
  307. package/scripts/rules/lane-split-park.ts +128 -0
  308. package/scripts/rules/oracle-firing.ts +1 -1
  309. package/scripts/rules/poll-loop.ts +1 -1
  310. package/scripts/rules/pre-gate-cut.ts +3 -3
  311. package/scripts/rules/protected-files.ts +59 -4
  312. package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
  313. package/scripts/rules/shell-body-trigger.ts +1 -1
  314. package/scripts/rules/stop-rules.ts +18 -7
  315. package/scripts/rules/test-declared-commit.ts +4 -4
  316. package/scripts/rules/wip-cap.ts +63 -0
  317. package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
  318. package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
  319. package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
  320. package/scripts/run/{run-carry.ts → carry/run-carry.ts} +12 -6
  321. package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
  322. package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +3 -3
  323. package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +2 -2
  324. package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
  325. package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
  326. package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
  327. package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
  328. package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
  329. package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +16 -2
  330. package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
  331. package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
  332. package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
  333. package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
  334. package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
  335. package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
  336. package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
  337. package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
  338. package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
  339. package/scripts/run/run-ending.ts +2 -2
  340. package/scripts/run/run-halfrun-resume.ts +6 -1
  341. package/scripts/run/run-headless.ts +1 -1
  342. package/scripts/run/run-invocation.ts +85 -84
  343. package/scripts/run/run-liveness.ts +1 -1
  344. package/scripts/run/run-parent-cut-hook.ts +1 -1
  345. package/scripts/run/run-preflight.ts +6 -6
  346. package/scripts/run/run-prep-cli.ts +1 -1
  347. package/scripts/run/run-prrun-resume.ts +1 -1
  348. package/scripts/run/run-report-cli.ts +4 -4
  349. package/scripts/run/run-report.ts +2 -2
  350. package/scripts/run/run-status-cli.ts +1 -1
  351. package/scripts/run/run-step-cli.ts +3 -3
  352. package/scripts/run/run-step.ts +2 -2
  353. package/scripts/run/run-stop-notify.ts +1 -1
  354. package/scripts/run/run-stranded-detect.ts +4 -4
  355. package/scripts/run/run-stranded.ts +2 -2
  356. package/scripts/run/run-watcher-guard-cli.ts +1 -1
  357. package/scripts/run/run-watcher-guard.ts +5 -3
  358. package/scripts/run/run-watcher-hook.ts +1 -1
  359. package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +29 -8
  360. package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
  361. package/scripts/run/ship/run-ship-lock.ts +50 -0
  362. package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +13 -0
  363. package/scripts/run/ship/run-ship-pre-detach.ts +66 -0
  364. package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
  365. package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
  366. package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +10 -6
  367. package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
  368. package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
  369. package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +13 -0
  370. package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +15 -8
  371. package/scripts/run/ship/run-ship-stop-text.ts +13 -0
  372. package/scripts/run/ship/run-ship-sync.ts +128 -0
  373. package/scripts/run/{run-ship.ts → ship/run-ship.ts} +6 -0
  374. package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
  375. package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +31 -9
  376. package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +3 -3
  377. package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +99 -17
  378. package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
  379. package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +8 -2
  380. package/scripts/run/wake/run-wake-restart.ts +58 -0
  381. package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
  382. package/scripts/run/{run-wake.ts → wake/run-wake.ts} +1 -1
  383. package/scripts/safe-chain/preinstall-command.ts +20 -21
  384. package/scripts/safe-chain/project-config.ts +19 -15
  385. package/scripts/sync/sync-ai-files.ts +1 -1
  386. package/scripts/sync/sync-configs.ts +2 -1
  387. package/scripts/sync/sync.ts +15 -2
  388. package/scripts/test/test-red-logic.ts +17 -2
  389. package/scripts/test/test-red.ts +8 -7
  390. package/scripts/test/unit-worker-share.ts +1 -0
  391. package/scripts/time-runtime/time-background.ts +5 -1
  392. package/scripts/time-runtime/time-instant.ts +3 -1
  393. package/scripts/time-runtime/time-spans.ts +6 -139
  394. package/scripts/time-runtime/time-tool-call.ts +160 -0
  395. package/scripts/time-runtime/time-transcript-line.ts +27 -1
  396. package/scripts/version/effective-upstream.ts +2 -20
  397. package/scripts/version/latest-corepack.ts +17 -17
  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 +2 -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/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
  419. /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
  420. /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
  421. /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
  422. /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
  423. /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
  424. /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
  425. /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
  426. /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
  427. /package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +0 -0
  428. /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
  429. /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
  430. /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
  431. /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
  432. /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
  433. /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
  434. /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
  435. /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
  436. /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
  437. /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
@@ -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
@@ -2,6 +2,7 @@ import { existsSync, globSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { find_local_bin_upwards } from '#scripts/build/local-bin'
4
4
  import { project_profile } from '#scripts/init/project-profile'
5
+ import { ancestor_directories } from '#scripts/lib/ancestor-directories'
5
6
  import { json_value } from '#scripts/lib/json-value'
6
7
  import { SKIP_MARKER } from '#scripts/test/skip-marker'
7
8
  import { z } from 'zod'
@@ -43,15 +44,11 @@ const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies']
43
44
  const object_schema = z.record(z.string(), z.unknown())
44
45
 
45
46
  function project_root(directory: string): string {
46
- let current = directory
47
-
48
- while (!existsSync(path.join(current, PACKAGE_JSON))) {
49
- const parent = path.dirname(current)
50
- if (parent === current) return directory
51
- current = parent
52
- }
53
-
54
- return current
47
+ return (
48
+ ancestor_directories.nearest(directory, (candidate) =>
49
+ existsSync(path.join(candidate, PACKAGE_JSON)),
50
+ ) ?? directory
51
+ )
55
52
  }
56
53
 
57
54
  function is_ignored(entry: string): boolean {
@@ -1,4 +1,5 @@
1
1
  import { find_local_bin_upwards } from '#scripts/build/local-bin'
2
+ import { COMMAND_TIMEOUT_MS } from '#scripts/lib/timeouts'
2
3
  import { execa } from 'execa'
3
4
 
4
5
  // joshuafolkken/kit#934: `josh gate`'s type-check step used to be `tsc --noEmit` for every project.
@@ -28,8 +29,6 @@ const TOOLKIT_BINS: ReadonlyArray<string> = ['josh-app', 'josh-game']
28
29
  // a bracketed warning printed first would otherwise be read as the command list.
29
30
  const USAGE_COMMANDS_PATTERN = /Usage:[^<>\n]*<([^<>\n]+)>/u
30
31
  const COMMAND_SEPARATOR = '|'
31
- // A toolkit that prints its usage answers in milliseconds; anything slower is hung.
32
- const USAGE_TIMEOUT_MS = 30_000
33
32
 
34
33
  function parse_usage_commands(output: string): ReadonlyArray<string> {
35
34
  const group = USAGE_COMMANDS_PATTERN.exec(output)?.[1]
@@ -46,7 +45,7 @@ async function read_toolkit_commands(bin_path: string): Promise<ReadonlyArray<st
46
45
  const result = await execa(bin_path, [], {
47
46
  all: true,
48
47
  reject: false,
49
- timeout: USAGE_TIMEOUT_MS,
48
+ timeout: COMMAND_TIMEOUT_MS,
50
49
  })
51
50
 
52
51
  return parse_usage_commands(result.all)
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'
4
4
  import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
5
5
  import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
6
6
  import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
7
+ import { lane_ledger } from '#scripts/lane/lane-ledger'
7
8
  import { bounded_pool } from '#scripts/lib/bounded-pool'
8
9
  import { buffered_process, FAIL_EXIT_CODE } from '#scripts/lib/buffered-process'
9
10
  import { review_stamps } from '#scripts/review/review-stamps'
@@ -166,7 +167,7 @@ interface BudgetContext {
166
167
  function default_budget(): BudgetContext {
167
168
  return {
168
169
  available_cores: availableParallelism(),
169
- is_reserved: !unit_worker_share.is_nested_run(),
170
+ is_reserved: !unit_worker_share.is_nested_run() && !core_budget.is_held(),
170
171
  }
171
172
  }
172
173
 
@@ -222,10 +223,17 @@ async function run_gate_steps(
222
223
  ): Promise<ReadonlyArray<GateStepResult>> {
223
224
  const reserved = await build_reserved_steps(process.cwd(), plan, budget.available_cores)
224
225
 
225
- return await bounded_pool.bounded_map(
226
- reserved,
227
- plan.concurrency,
228
- async (entry) => await run_reserved_step(entry, budget),
226
+ // **The checks run under the held mark, so a check's own dispatch claims nothing**
227
+ // (joshuafolkken/kit#3345). Each check is a `pnpm josh <target>` child, and a target that declares
228
+ // a weight would otherwise claim a second place for the cores reserved for it here. A nested gate
229
+ // sets the mark too, since its checks are covered by the outer gate's reservation.
230
+ return await core_budget.with_held_mark(
231
+ async () =>
232
+ await bounded_pool.bounded_map(
233
+ reserved,
234
+ plan.concurrency,
235
+ async (entry) => await run_reserved_step(entry, budget),
236
+ ),
229
237
  )
230
238
  }
231
239
 
@@ -299,6 +307,9 @@ interface GateOptions {
299
307
  // The scoped records' paths, overridable so a test can plant a green pair rather than depend on the
300
308
  // surrounding checkout's — the reason `scoped-green.ts` takes a `source` per stamp.
301
309
  scoped_sources?: ScopedSources
310
+ // The lane ledger a finished gate's duration is appended to (joshuafolkken/kit#3355). Only the CLI
311
+ // entry resolves it, so a suite driving the gate never writes a measurement of checks that never ran.
312
+ ledger_path?: string | undefined
302
313
  }
303
314
 
304
315
  // **The gate refuses to start when the scoped pair has not been green on this tree**
@@ -339,14 +350,20 @@ async function settle_gate(
339
350
  tree: GateTree,
340
351
  options: GateOptions & { started_at: number },
341
352
  ): Promise<number> {
353
+ const elapsed_ms = performance.now() - options.started_at
342
354
  const failed_labels = gate_report.report_gate_steps(results, {
343
355
  is_verbose: options.is_verbose ?? false,
344
356
  log_path: options.log_path,
345
- elapsed_ms: performance.now() - options.started_at,
357
+ elapsed_ms,
346
358
  step_count: String(plan.checks.length),
347
359
  })
360
+ const is_passed = failed_labels.length === 0
348
361
 
349
- if (failed_labels.length > 0) return FAIL_EXIT_CODE
362
+ if (options.ledger_path !== undefined) {
363
+ await lane_ledger.record_gate(options.ledger_path, elapsed_ms, is_passed)
364
+ }
365
+
366
+ if (!is_passed) return FAIL_EXIT_CODE
350
367
 
351
368
  await record_whole_gate(plan, results, tree, options)
352
369
 
@@ -474,7 +491,9 @@ async function run_gate_command(
474
491
  // summary, written last, is the first thing lost. Setting the code lets the writes drain and the
475
492
  // process end on its own, which it can, since every child has already exited by here.
476
493
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
477
- process.exitCode = await run_gate_command(process.argv.slice(FIRST_ARGUMENT_INDEX))
494
+ process.exitCode = await run_gate_command(process.argv.slice(FIRST_ARGUMENT_INDEX), {
495
+ ledger_path: await lane_ledger.target(),
496
+ })
478
497
  }
479
498
 
480
499
  const verification_gate = {
@@ -14,8 +14,8 @@ const FULL_PAGE_QUERY = '?per_page=100'
14
14
  // `gh api` expands `{owner}` and `{repo}` from the current repository, so the unqualified form needs
15
15
  // no extra lookup to name a path. An explicit `repo` names another repository — the form a
16
16
  // cross-repository reference takes.
17
- function repo_api_path(repo?: string): string {
18
- return `repos/${repo ?? '{owner}/{repo}'}`
17
+ function repo_api_path(repo = '{owner}/{repo}'): string {
18
+ return `repos/${repo}`
19
19
  }
20
20
 
21
21
  // The issue collection — what a listing pages through and what a creation posts to. One segment,
@@ -1,8 +1,8 @@
1
+ import { blocking_issue_schema } from '#scripts/git/git-schemas'
1
2
  import {
2
3
  parse_json_array_or_undefined,
3
4
  parse_json_object_safe,
4
5
  } from '#scripts/git/parse-json-array'
5
- import { blocking_issue_schema } from '#scripts/git/schemas'
6
6
  import type { IssueReference } from '#scripts/issue/issue-reference'
7
7
  import { z } from 'zod'
8
8
  import { MERGED_STATE, to_gh_state } from './git-gh-rest-state'
@@ -163,8 +163,8 @@ function empty_blocked_by(): BlockedBy {
163
163
  // repository and names a different issue there (joshuafolkken/kit#1126). Undefined when the field is
164
164
  // absent or shaped otherwise; the caller then falls back to the repository it is reading in, which is
165
165
  // what an unqualified relation has always meant.
166
- function repo_of_url(repository_url: string | undefined): string | undefined {
167
- const match = REPOSITORY_URL.exec(repository_url ?? '')
166
+ function repo_of_url(repository_url: string | undefined = ''): string | undefined {
167
+ const match = REPOSITORY_URL.exec(repository_url)
168
168
 
169
169
  return match?.[REPOSITORY_URL_GROUP]
170
170
  }
@@ -29,6 +29,7 @@ const DELETE_METHOD = 'DELETE'
29
29
  // object, so the same value is unwrapped out of it rather than the shape being changed.
30
30
  const HTML_URL_FILTER = '.html_url'
31
31
  const ID_FILTER = '.id'
32
+ const LABEL_NAMES_FILTER = '.[].name'
32
33
 
33
34
  const LABELS_SEGMENT = '/labels'
34
35
  const COMMENTS_SEGMENT = '/comments'
@@ -182,17 +183,60 @@ async function issue_create_with_label(input: {
182
183
  )
183
184
  }
184
185
 
186
+ // The outcome of applying one label, with the swallowed failure's gist kept for a caller that has to
187
+ // say why it refused (joshuafolkken/kit#3312).
188
+ type LabelWrite = { is_applied: true } | { is_applied: false; reason: string }
189
+
190
+ // The issue's label names, read back after a failed write. A read that itself fails answers
191
+ // `undefined`, which the caller treats as "not applied" — the write's own error is the reason then.
192
+ async function read_label_names(issue_number: string): Promise<Array<string> | undefined> {
193
+ try {
194
+ const raw = await git_gh_exec.exec_gh_api({
195
+ path: `${git_gh_api_path.issue_api_path(issue_number)}${LABELS_SEGMENT}`,
196
+ should_paginate: true,
197
+ jq_filter: LABEL_NAMES_FILTER,
198
+ })
199
+
200
+ return raw.split('\n').map((name) => name.trim())
201
+ } catch (error) {
202
+ error_text.trace_swallowed('git_gh_issue_write.read_label_names', error)
203
+
204
+ return undefined
205
+ }
206
+ }
207
+
208
+ // **A failed answer is not a failed write.** The POST is idempotent, and a timeout, a non-zero exit
209
+ // after the write or a secondary rate limit can each throw after the label landed — which refused a
210
+ // lane dispatch whose label was in fact applied (joshuafolkken/kit#3312). So a throw is confirmed by
211
+ // reading the labels back, and only a label that is really absent answers `is_applied: false`.
212
+ async function issue_apply_label(issue_number: string, label: string): Promise<LabelWrite> {
213
+ const labels_path = `${git_gh_api_path.issue_api_path(issue_number)}${LABELS_SEGMENT}`
214
+
215
+ try {
216
+ await git_gh_exec.exec_gh_api({ path: labels_path, body: JSON.stringify({ labels: [label] }) })
217
+
218
+ return { is_applied: true }
219
+ } catch (error) {
220
+ const names = await read_label_names(issue_number)
221
+
222
+ if (names?.includes(label) === true) return { is_applied: true }
223
+ error_text.trace_swallowed('git_gh_issue_write.issue_apply_label', error)
224
+
225
+ // The first line is the gist: gh follows it with the response body, which a one-line refusal
226
+ // does not need.
227
+ const [gist = ''] = error_text.message_of(error).split('\n', 1)
228
+
229
+ return { is_applied: false, reason: gist }
230
+ }
231
+ }
232
+
185
233
  // Applied after the body edit so a failure leaves an issue with the epic sections and no label,
186
234
  // which `epic:check` reports — rather than a labelled issue with nothing to track. The caller checks
187
235
  // the return: the label is what the auto-close filters on (joshuafolkken/kit#865).
188
236
  async function issue_add_label(issue_number: string, label: string): Promise<boolean> {
189
- return await did_write_succeed(
190
- async () =>
191
- await git_gh_exec.exec_gh_api({
192
- path: `${git_gh_api_path.issue_api_path(issue_number)}${LABELS_SEGMENT}`,
193
- body: JSON.stringify({ labels: [label] }),
194
- }),
195
- )
237
+ const write = await issue_apply_label(issue_number, label)
238
+
239
+ return write.is_applied
196
240
  }
197
241
 
198
242
  // The counterpart to the addition above, for a run taking its own marker back off
@@ -279,6 +323,7 @@ const git_gh_issue_write = {
279
323
  label_ensure,
280
324
  issue_create_request,
281
325
  issue_create_with_label,
326
+ issue_apply_label,
282
327
  issue_add_label,
283
328
  issue_remove_label,
284
329
  issue_add_blocked_by,
@@ -286,3 +331,4 @@ const git_gh_issue_write = {
286
331
  }
287
332
 
288
333
  export { git_gh_issue_write }
334
+ export type { LabelWrite }
@@ -26,8 +26,10 @@ const SUMMARY_FIELDS = 'number,title,labels,createdAt'
26
26
  // `SUMMARY_FIELDS` on purpose: a listing carries no `blockedBy`, so the field is answered from each
27
27
  // row's own dependencies endpoint, and asking for it on the shared list would put that request
28
28
  // behind the next-issues display too — where a refused one becomes a failure with no message at all,
29
- // since the listing swallows the error (joshuafolkken/kit#996, joshuafolkken/kit#1025).
30
- const PICKUP_FIELDS = `${SUMMARY_FIELDS},blockedBy`
29
+ // since the listing swallows the error (joshuafolkken/kit#996, joshuafolkken/kit#1025). `body` costs
30
+ // no request — the REST listing row already carries it — and is what the backlog offer reads for
31
+ // the restructures each issue declares (joshuafolkken/kit#3221).
32
+ const PICKUP_FIELDS = `${SUMMARY_FIELDS},blockedBy,body`
31
33
 
32
34
  // Every listing below goes through the one REST invocation in `git-gh-issue-list.ts`. It kept the
33
35
  // `json_fields` / `limit` contract it had as a `gh issue list` wrapper, so these callers changed
@@ -111,7 +113,7 @@ async function issue_list_by_label(label: string, limit: number): Promise<IssueL
111
113
  }
112
114
 
113
115
  // The same filter with the fields the `auto-ok` pickup needs (joshuafolkken/kit#906): it orders by
114
- // `createdAt` and re-checks `labels` client-side, so bodies would be fetched and thrown away. A
116
+ // `createdAt`, re-checks `labels` client-side and reads each body for its declared restructures. A
115
117
  // label that does not exist in the repository is not an error — the listing comes back empty, which
116
118
  // is exactly "nobody has opted anything in".
117
119
  // `json_fields` overrides the pickup's list. Its only caller is the probe that runs *after* a failed