@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
@@ -1,4 +1,5 @@
1
1
  import { realpathSync } from 'node:fs'
2
+ import path from 'node:path'
2
3
  import { pathToFileURL } from 'node:url'
3
4
  import type { CommandEntry } from './josh-command-types'
4
5
 
@@ -8,26 +9,44 @@ import type { CommandEntry } from './josh-command-types'
8
9
  // (joshuafolkken/kit#1342). The script is ordinary TypeScript and a TypeScript loader is already
9
10
  // installed by the time this file runs, so the dispatcher evaluates the script itself instead.
10
11
  const TYPESCRIPT_EXTENSION = '.ts'
12
+ const JAVASCRIPT_EXTENSION = '.js'
13
+ const BUNDLED_COMMAND_DIRECTORY = path.join('dist', 'commands')
11
14
  const FAILURE_EXIT_CODE = 1
12
15
 
13
16
  // The dispatcher may only load a `.ts` script in its own process when it was itself loaded as
14
17
  // TypeScript — which is exactly the condition under which a loader capable of it is present. A
15
- // consumer runs the bundled `dist/josh.js` under plain node, whose `import.meta.url` ends in `.js`,
16
- // so that install keeps the spawning path unchanged rather than failing on a module node cannot
17
- // load at all.
18
+ // consumer runs the bundled `dist/josh.js` under plain node, whose `import.meta.url` ends in `.js`.
18
19
  function is_typescript_dispatcher(dispatcher_url: string): boolean {
19
20
  return dispatcher_url.endsWith(TYPESCRIPT_EXTENSION)
20
21
  }
21
22
 
23
+ // A command marked `is_bundled` is pre-built to `dist/commands/<script basename>.js` by
24
+ // `scripts/build/build-commands.ts` (joshuafolkken/kit#3328). The path is derived here, in a module
25
+ // the dispatcher bundle already carries, so the build and the dispatcher cannot name two locations.
26
+ function bundled_script_path(package_directory: string, script: string): string {
27
+ const name = `${path.basename(script, TYPESCRIPT_EXTENSION)}${JAVASCRIPT_EXTENSION}`
28
+
29
+ return path.join(package_directory, BUNDLED_COMMAND_DIRECTORY, name)
30
+ }
31
+
32
+ // The file this dispatcher imports in its own process, or `undefined` to spawn tsx instead.
22
33
  // `tsx_arguments` are node flags the script needs *before* its own code runs — today every one of
23
34
  // them a form of `--env-file`, which has no in-process equivalent that reproduces node's own
24
35
  // parsing and precedence. Those commands (`doctor`, `latest:scope`, `followup`, `notify`) keep a
25
36
  // process of their own; each runs at most a few times per run, so none of them is where the cost
26
- // this saves accumulates.
27
- function can_run_in_process(entry: CommandEntry, dispatcher_url: string): boolean {
28
- if (entry.script === undefined || entry.tsx_arguments !== undefined) return false
37
+ // this saves accumulates. Under plain node, which cannot load a `.ts` script at all, only a
38
+ // pre-built command runs in-process; every other one keeps spawning tsx.
39
+ function in_process_target(
40
+ entry: CommandEntry,
41
+ package_directory: string,
42
+ dispatcher_url: string,
43
+ ): string | undefined {
44
+ if (entry.script === undefined || entry.tsx_arguments !== undefined) return undefined
45
+ if (is_typescript_dispatcher(dispatcher_url)) return path.join(package_directory, entry.script)
29
46
 
30
- return is_typescript_dispatcher(dispatcher_url)
47
+ return entry.is_bundled === true
48
+ ? bundled_script_path(package_directory, entry.script)
49
+ : undefined
31
50
  }
32
51
 
33
52
  // `process.exitCode` is typed `number | string | undefined`. Only the numeric form is an exit code
@@ -75,6 +94,6 @@ async function run_in_process(
75
94
  return read_exit_code()
76
95
  }
77
96
 
78
- const josh_in_process = { can_run_in_process, run_in_process }
97
+ const josh_in_process = { bundled_script_path, in_process_target, run_in_process }
79
98
 
80
- export { FAILURE_EXIT_CODE, josh_in_process }
99
+ export { BUNDLED_COMMAND_DIRECTORY, FAILURE_EXIT_CODE, josh_in_process }
@@ -2,6 +2,8 @@ import { existsSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { resolve_local_bin, resolve_package_bin } from '#scripts/build/local-bin'
5
+ import { core_budget } from '#scripts/gate/core-budget'
6
+ import { ancestor_directories } from '#scripts/lib/ancestor-directories'
5
7
  import { package_version_schema } from '#scripts/lib/schemas'
6
8
  import { resolve_spawn_exit } from '#scripts/lib/spawn-exit'
7
9
  import { execaSync } from 'execa'
@@ -37,15 +39,11 @@ interface TsxRunner {
37
39
  // from the bundled dist/josh.js (one level under the root) and from the tsx source at
38
40
  // scripts/josh/ (two levels), so this file's depth no longer has to be hard-coded.
39
41
  function find_package_directory(start_directory: string): string {
40
- let current = start_directory
41
-
42
- while (!existsSync(path.join(current, PACKAGE_JSON))) {
43
- const parent = path.dirname(current)
44
- if (parent === current) return start_directory
45
- current = parent
46
- }
47
-
48
- return current
42
+ return (
43
+ ancestor_directories.nearest(start_directory, (directory) =>
44
+ existsSync(path.join(directory, PACKAGE_JSON)),
45
+ ) ?? start_directory
46
+ )
49
47
  }
50
48
 
51
49
  const PACKAGE_DIR = find_package_directory(path.dirname(fileURLToPath(import.meta.url)))
@@ -226,14 +224,12 @@ async function run_script_entry(
226
224
  entry: CommandEntry,
227
225
  subcommand_arguments: Array<string>,
228
226
  ): Promise<number> {
229
- const script_path = path.join(PACKAGE_DIR, entry.script ?? '')
230
227
  const script_arguments = [...(entry.default_script_arguments ?? []), ...subcommand_arguments]
228
+ const target = josh_in_process.in_process_target(entry, PACKAGE_DIR, import.meta.url)
231
229
 
232
- if (josh_in_process.can_run_in_process(entry, import.meta.url)) {
233
- return await josh_in_process.run_in_process(script_path, script_arguments)
234
- }
230
+ if (target !== undefined) return await josh_in_process.run_in_process(target, script_arguments)
235
231
 
236
- return spawn_script_entry(entry, script_path, script_arguments)
232
+ return spawn_script_entry(entry, path.join(PACKAGE_DIR, entry.script ?? ''), script_arguments)
237
233
  }
238
234
 
239
235
  function github_prerequisite_exit(resolved: string, is_consumer: boolean): number | undefined {
@@ -274,7 +270,7 @@ function pre_dispatch_exit(
274
270
  return USAGE_ERROR_EXIT_CODE
275
271
  }
276
272
 
277
- async function dispatch_entry(
273
+ async function run_entry(
278
274
  entry: CommandEntry,
279
275
  subcommand_arguments: Array<string>,
280
276
  ): Promise<number> {
@@ -283,6 +279,28 @@ async function dispatch_entry(
283
279
  return await run_script_entry(entry, subcommand_arguments)
284
280
  }
285
281
 
282
+ function command_weight(entry: CommandEntry): number | undefined {
283
+ return typeof entry.core_weight === 'function' ? entry.core_weight() : entry.core_weight
284
+ }
285
+
286
+ // **Every command passes through here, so this is where a declared weight is reserved**
287
+ // (joshuafolkken/kit#3345). A heavy command run directly, outside `josh gate`, used to start its tools
288
+ // without claiming a place. A command under a parent that already holds its cores claims nothing:
289
+ // `core_budget.with_command_reservation` reads the mark that parent set.
290
+ async function dispatch_entry(
291
+ entry: CommandEntry,
292
+ subcommand_arguments: Array<string>,
293
+ ): Promise<number> {
294
+ const weight = command_weight(entry)
295
+
296
+ if (weight === undefined) return await run_entry(entry, subcommand_arguments)
297
+
298
+ return await core_budget.with_command_reservation(
299
+ weight,
300
+ async () => await run_entry(entry, subcommand_arguments),
301
+ )
302
+ }
303
+
286
304
  async function run_command(
287
305
  cmd: string,
288
306
  subcommand_arguments: Array<string>,
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto'
3
3
  import { readFileSync, rmSync } from 'node:fs'
4
4
  import { createServer, type Server } from 'node:net'
5
5
  import path from 'node:path'
6
+ import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
6
7
  import { PLATFORM_TEMP_ROOT } from './platform-temporary'
7
8
 
8
9
  // Whether the process a record names is still the one that wrote it (joshuafolkken/kit#1245).
@@ -48,9 +49,6 @@ const PID_FLAG = '-p'
48
49
  // runs in** — nothing is inherited, so the token depends on the process being asked about and on
49
50
  // nothing whatever about who is asking.
50
51
  const PROBE_ENVIRONMENT = { TZ: 'UTC', LC_ALL: 'C' }
51
- // A probe that hung would hang `josh review:brief`, `josh bench` and the unit suite's worker share
52
- // alike. Failing is harmless here: it answers "cannot tell", which every caller already handles.
53
- const PROBE_TIMEOUT_MS = 2000
54
52
  // `process.kill(pid, 0)` runs every permission check and delivers nothing, so it is the standard
55
53
  // liveness probe: it throws `ESRCH` where the process is gone.
56
54
  const LIVENESS_SIGNAL = 0
@@ -20,16 +20,16 @@ import { session_language, type Resolution } from './session-language'
20
20
 
21
21
  const { DEFAULT_SESSION_LANG, ENV_KEY, resolve_session_lang } = session_language
22
22
  const ENGLISH_OPT_IN_LANG = 'en'
23
- const SCOPE_NOTE =
24
- 'write session dialogue, AskUserQuestion labels/descriptions and artifact prose (Issue bodies, comments, Telegram) in this language. English stays only for Issue/PR titles, code comments/test titles/commit messages, and script-fixed strings.'
25
23
 
26
24
  // The file load happens in `main`, on the command path only; the resolver reads the environment.
25
+ // The line carries the value alone: what the language covers is `CLAUDE.md` → "Output language",
26
+ // already in context every turn, so repeating it here only spent context on each prompt.
27
27
  function format_line(resolution: Resolution): string {
28
28
  const suffix = resolution.is_default
29
29
  ? ` (default; set ${ENV_KEY}=${ENGLISH_OPT_IN_LANG} for English)`
30
30
  : ''
31
31
 
32
- return `Session language (${ENV_KEY}): ${resolution.lang}${suffix} — ${SCOPE_NOTE}`
32
+ return `Session language (${ENV_KEY}): ${resolution.lang}${suffix}`
33
33
  }
34
34
 
35
35
  function main(): void {
@@ -42,7 +42,6 @@ const session_language_cli = {
42
42
  ENV_KEY,
43
43
  format_line,
44
44
  resolve_session_lang,
45
- SCOPE_NOTE,
46
45
  }
47
46
 
48
47
  if (process.argv[1] === fileURLToPath(import.meta.url)) main()
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process'
2
2
  import { setTimeout as sleep } from 'node:timers/promises'
3
3
  import { parseArgs } from 'node:util'
4
+ import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
4
5
  import { run_liveness } from '#scripts/run/run-liveness'
5
6
  import { lane_child_invocation } from './lane-child-invocation'
6
7
  import { lane_handoff } from './lane-handoff'
@@ -66,7 +67,10 @@ interface RunConfig extends CheckConfig {
66
67
 
67
68
  function is_process_running_default(issue: string): boolean {
68
69
  const pattern = lane_child_invocation.process_pattern(issue)
69
- const result = spawnSync('pgrep', ['-f', pattern], { encoding: 'utf8' })
70
+ const result = spawnSync('pgrep', ['-f', pattern], {
71
+ encoding: 'utf8',
72
+ timeout: PROBE_TIMEOUT_MS,
73
+ })
70
74
 
71
75
  if (result.status === PROCESS_FOUND) return true
72
76
 
@@ -1,6 +1,6 @@
1
1
  import { run_issue_number } from '#scripts/run/run-issue-number'
2
- import { run_ship_next, type ShipResume } from '#scripts/run/run-ship-next'
3
- import type { Stage } from '#scripts/run/run-ship-stage'
2
+ import { run_ship_next, type ShipResume } from '#scripts/run/ship/run-ship-next'
3
+ import type { Stage } from '#scripts/run/ship/run-ship-stage'
4
4
 
5
5
  interface ShipStop extends ShipResume {
6
6
  stage: Stage
@@ -2,11 +2,11 @@
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { epic_busy } from '#scripts/epic/epic-busy'
4
4
  import { git_gh_command } from '#scripts/gh/git-gh-command'
5
- import type { OpenIssueData } from '#scripts/git/schemas'
5
+ import type { OpenIssueData } from '#scripts/git/git-schemas'
6
6
  import { issue_cite, type IssueCiter } from '#scripts/issue/issue-cite'
7
7
  import { error_text } from '#scripts/lib/error-message'
8
- import { run_carry } from '#scripts/run/run-carry'
9
- import { run_carry_reclaim } from '#scripts/run/run-carry-reclaim'
8
+ import { run_carry } from '#scripts/run/carry/run-carry'
9
+ import { run_carry_reclaim } from '#scripts/run/carry/run-carry-reclaim'
10
10
  import { lane_await } from './lane-await'
11
11
  import { lane_close, type CloseOutcome, type SweepOutcome } from './lane-close'
12
12
  import { lane_dispatch, type DispatchOutcome } from './lane-dispatch'
@@ -1,12 +1,13 @@
1
1
  import { agent_argv, type AgentArgv, type AgentArgvResult } from '#scripts/agent/agent-argv'
2
2
  import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role-profile'
3
3
  import { git_gh_command } from '#scripts/gh/git-gh-command'
4
+ import type { LabelWrite } from '#scripts/gh/git-gh-issue-write'
4
5
  import { IN_PROGRESS_LABEL } from '#scripts/issue/issue-labels'
5
6
  import { telegram_notify } from '#scripts/notify/telegram-notify'
6
7
  import { detached_launch } from '#scripts/run/detached-launch'
8
+ import { run_event_stream } from '#scripts/run/event/run-event-stream'
9
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
7
10
  import { run_ending } from '#scripts/run/run-ending'
8
- import { run_event_stream } from '#scripts/run/run-event-stream'
9
- import { run_event_stream_emit } from '#scripts/run/run-event-stream-emit'
10
11
  import { run_issue_number } from '#scripts/run/run-issue-number'
11
12
  import { lane_child_invocation } from './lane-child-invocation'
12
13
  import { lane_child_marker } from './lane-child-marker'
@@ -79,7 +80,7 @@ type DispatchOutcome =
79
80
  // that marks a child as holding its lane — the lane count, the offer filter, `run:progress` and the
80
81
  // stale check all read it — so a child that ran without it would be counted as an empty lane and
81
82
  // handed to a second session. Refusing the launch keeps "dispatched" and "counted as busy" one state.
82
- | { kind: typeof LABEL_UNSET_KIND }
83
+ | { kind: typeof LABEL_UNSET_KIND; reason: string }
83
84
 
84
85
  type LogOutcome = { kind: 'ready'; path: string } | { kind: 'unrecordable'; reason: string }
85
86
 
@@ -305,9 +306,10 @@ function prepared(lane: LaneInfo): Prepared {
305
306
  // to its (idempotent) apply. `issue_add_label` creates the label if the repository lacks it — a
306
307
  // `POST /issues/{N}/labels` provisions a missing label with a generated color (measured on
307
308
  // joshuafolkken/kit#1026, `git-gh-issue-write.ts`) — so no separate `label_ensure` is needed here, and
308
- // its `false` is a real refusal to launch rather than a note.
309
- async function mark_in_progress(issue: string): Promise<boolean> {
310
- return await git_gh_command.issue_add_label(issue, IN_PROGRESS_LABEL)
309
+ // its unapplied answer is a real refusal to launch rather than a note — confirmed by a read-back, so
310
+ // a write that landed but answered with an error still launches (joshuafolkken/kit#3312).
311
+ async function mark_in_progress(issue: string): Promise<LabelWrite> {
312
+ return await git_gh_command.issue_apply_label(issue, IN_PROGRESS_LABEL)
311
313
  }
312
314
 
313
315
  // The dispatch owns both halves of the marker: it applied it, so a launch that never started takes it
@@ -324,8 +326,12 @@ async function unmark_in_progress(issue: string): Promise<void> {
324
326
 
325
327
  // A started child is recorded on the run's stream, which is where the stall detector ages the last
326
328
  // dispatch from (joshuafolkken/kit#2464) — unrecorded, every stall read "hours since the last dispatch"
327
- // minutes after a launch. A failed start records nothing: no child is running.
329
+ // minutes after a launch. A failed start records nothing: no child is running. The marker is claimed
330
+ // first, so a refused one starts nothing.
328
331
  async function finish_dispatch(issue: string, request: StartRequest): Promise<DispatchOutcome> {
332
+ const mark = await mark_in_progress(issue)
333
+
334
+ if (!mark.is_applied) return { kind: LABEL_UNSET_KIND, reason: mark.reason }
329
335
  const outcome = await started(request)
330
336
 
331
337
  const is_failed = outcome.kind === 'failed'
@@ -353,8 +359,6 @@ async function dispatch_child(issue: string): Promise<DispatchOutcome> {
353
359
 
354
360
  if (log.kind !== 'ready') return log
355
361
 
356
- if (!(await mark_in_progress(issue))) return { kind: LABEL_UNSET_KIND }
357
-
358
362
  return await finish_dispatch(issue, { lane, log_path: log.path, ...built })
359
363
  }
360
364
 
@@ -410,7 +414,7 @@ function describe(outcome: DispatchOutcome, issue: string): string {
410
414
  if (outcome.kind === 'unrecordable') return outcome.reason
411
415
 
412
416
  if (outcome.kind === LABEL_UNSET_KIND) {
413
- return `The child for #${issue} was not started: the \`${IN_PROGRESS_LABEL}\` label could not be applied, so the lane would be counted as empty. Nothing was launched.`
417
+ return `The child for #${issue} was not started: the \`${IN_PROGRESS_LABEL}\` label could not be applied (${outcome.reason}), so the lane would be counted as empty. Nothing was launched.`
414
418
  }
415
419
 
416
420
  if (outcome.kind === 'failed') {
@@ -185,9 +185,13 @@ function replace_assignment(
185
185
  key: string,
186
186
  ): Array<string> {
187
187
  return lines.flatMap((entry, index) => {
188
+ // One return type across the branches (sonarjs/function-return-type) keeps the array wrappers.
189
+ // eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- see above
188
190
  if (index === keep) return [assignment]
191
+ if (is_key_line(entry, key)) return []
189
192
 
190
- return is_key_line(entry, key) ? [] : [entry]
193
+ // eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- see above
194
+ return [entry]
191
195
  })
192
196
  }
193
197
 
@@ -1,5 +1,5 @@
1
1
  import { git_common_directory } from '#scripts/git/git-common-directory'
2
- import { run_ship_detach } from '#scripts/run/run-ship-detach'
2
+ import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
3
3
  import { lane_child_marker, type MarkerSource } from './lane-child-marker'
4
4
 
5
5
  // Whether a lane has handed its post-implementation region to the detached ship (joshuafolkken/kit#2962).
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { buffered_process } from '#scripts/lib/buffered-process'
4
+ import { INSTALL_TIMEOUT_MS } from '#scripts/lib/timeouts'
4
5
 
5
6
  // Filling the lane, which is the other half of opening one (joshuafolkken/kit#1554).
6
7
  //
@@ -34,12 +35,6 @@ const MANIFEST_FILE_NAME = 'package.json'
34
35
  // The lane opens with no dependencies installed, and that is the reported state rather than a
35
36
  // failure — there was nothing to install, so nothing failed.
36
37
  const NO_MANIFEST_OUTPUT = `No ${MANIFEST_FILE_NAME} in the lane, so the dependency install was skipped.`
37
- // The 3.5 s above is a warm store; a first run, or one after a dependency change, goes to the
38
- // network, so that figure is not the bound to write here. Ten minutes covers that fetch and still
39
- // ends `lane:open` when a registry never answers, rather than holding a parallel run open with
40
- // nothing printed. It is well under the fan-out's own half hour because this one blocks a command a
41
- // person is waiting on, not a check suite.
42
- const INSTALL_TIMEOUT_MS = 600_000
43
38
 
44
39
  interface InstallResult {
45
40
  is_installed: boolean
@@ -85,10 +80,4 @@ async function install_dependencies(directory: string): Promise<InstallResult> {
85
80
  const lane_install = { install_dependencies }
86
81
 
87
82
  export type { InstallResult }
88
- export {
89
- lane_install,
90
- INSTALL_ARGUMENTS,
91
- INSTALL_TIMEOUT_MS,
92
- MANIFEST_FILE_NAME,
93
- NO_MANIFEST_OUTPUT,
94
- }
83
+ export { lane_install, INSTALL_ARGUMENTS, MANIFEST_FILE_NAME, NO_MANIFEST_OUTPUT }
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { josh_command } from '#scripts/josh/josh-run'
4
+ import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
5
+ import { lane_vacant } from '#scripts/lane/lane-vacant'
4
6
  import { cli_flags } from '#scripts/lib/cli-flags'
5
7
  import { INSTALL_TIMEOUT_MS } from '#scripts/lib/timeouts'
6
8
  import { execa } from 'execa'
@@ -15,6 +17,7 @@ import { execa } from 'execa'
15
17
  // and messages are reused rather than cloned — `lane:open`'s cut-session guard and its `full` /
16
18
  // `already-open` refusals, `stash:pop`'s message-matched pop, `lane:dispatch`'s in-progress claim and
17
19
  // warning. Their explanations stream through to stderr; the child's pid is the one thing on stdout.
20
+ // A lane a park kept is the one `already-open` it steps past — `lane_directory` below.
18
21
 
19
22
  const ARGV_OFFSET = 2
20
23
  const SUCCESS_EXIT_CODE = 0
@@ -86,14 +89,43 @@ type LaunchOutcome = { kind: 'launched'; pid: string } | { kind: 'unopened' } |
86
89
  const UNOPENED: LaunchOutcome = { kind: 'unopened' }
87
90
  const FAILED: LaunchOutcome = { kind: 'failed' }
88
91
 
92
+ async function close_if_vacant(lane: LaneInfo | undefined): Promise<void> {
93
+ if (lane === undefined || !(await lane_vacant.is_vacant(lane))) return
94
+
95
+ await josh_command.josh_run(['lane:close', lane.issue], should_forward_stderr)
96
+ }
97
+
98
+ // **A lane a park kept open is resumed, not reopened** (joshuafolkken/kit#3228). A child parked before
99
+ // its commit leaves its uncommitted work in its lane, and `lane:open` refuses that lane as
100
+ // `already-open` — so the released child is dispatched into the kept tree instead. **Only a lane a
101
+ // child was already dispatched into counts as kept**: `lane:dispatch` records the child's output in the
102
+ // lane's `.env`, and a lane whose install failed never got that far, so it still goes through
103
+ // `lane:open` and its refusal. So does a stranded lane, whose `.env` is gone with its tree.
104
+ //
105
+ // **A lane no child was dispatched into and that holds nothing is closed and opened afresh**
106
+ // (joshuafolkken/kit#3289) — a run cut between the open and the dispatch leaves one. It is reopened
107
+ // rather than reused because nothing recorded whether its install finished. A lane with work or a live
108
+ // process in it still meets `lane:open`'s refusal.
109
+ async function lane_directory(issue: string): Promise<string | undefined> {
110
+ const kept = await lane_registry.find_open_lane(issue)
111
+
112
+ if (kept?.output !== undefined) return kept.directory
113
+
114
+ await close_if_vacant(kept)
115
+
116
+ const opened = await josh_command.josh_run(['lane:open', issue], should_forward_stderr)
117
+
118
+ return opened.code === SUCCESS_EXIT_CODE ? opened.out : undefined
119
+ }
120
+
89
121
  // The launch chain in-process. The CLI prints the pid; `backlog:drive` launches through this same
90
122
  // chain (joshuafolkken/kit#2508).
91
123
  async function launch_lane(context: LaunchContext): Promise<LaunchOutcome> {
92
- const opened = await josh_command.josh_run(['lane:open', context.issue], should_forward_stderr)
124
+ const directory = await lane_directory(context.issue)
93
125
 
94
- if (opened.code !== SUCCESS_EXIT_CODE) return UNOPENED
126
+ if (directory === undefined) return UNOPENED
95
127
 
96
- if (!(await prepare(context.stash, opened.out))) return FAILED
128
+ if (!(await prepare(context.stash, directory))) return FAILED
97
129
 
98
130
  const dispatched = await josh_command.josh_run(
99
131
  ['lane:dispatch', context.issue],
@@ -0,0 +1,145 @@
1
+ import { closeSync, constants, openSync, writeSync } from 'node:fs'
2
+ import { stamp_file } from '#scripts/josh/stamp-file'
3
+ import { json_value } from '#scripts/lib/json-value'
4
+ import { run_carry } from '#scripts/run/carry/run-carry'
5
+ import { z } from 'zod'
6
+
7
+ // The measurement ledger the lane limit is tuned against (joshuafolkken/kit#3355): one JSON line per
8
+ // lane merge, per finished `josh gate`, and per machine-load sample, read back by `josh lane:stats`.
9
+ //
10
+ // **Its own file rather than the run's event stream.** The stream is capped at 500 events and is
11
+ // rewritten whole on every append, which is right for "where is the run" and wrong for a measurement
12
+ // that spans days of one-a-minute load samples. This file is only ever appended to, so concurrent lanes
13
+ // never lose each other's lines to a read-modify-write race.
14
+ //
15
+ // **Keyed on the common git directory, the event stream's key**, so a lane child's gate, the parent's
16
+ // merge and the sampler all land in one ledger per repository. It sits in the temp root beside every
17
+ // other josh record; a reboot clears it, which is why `docs/maintainers/lane-limit-measurement.md`
18
+ // closes a period with `josh lane:stats` before one.
19
+
20
+ const LEDGER_PREFIX = 'josh-lane-ledger-'
21
+ const LEDGER_SUFFIX = '.jsonl'
22
+ const LINE_SEPARATOR = '\n'
23
+ const LEDGER_FILE_MODE = 0o600
24
+ // The flags `detached-launch.ts` appends its log with, for the same deterministic shared-temp path.
25
+ const { O_APPEND, O_CREAT, O_NOFOLLOW, O_WRONLY } = constants
26
+ // eslint-disable-next-line no-bitwise -- POSIX open flags are a bit field; `||` here would be a bug
27
+ const APPEND_FLAGS = O_WRONLY | O_APPEND | O_CREAT | O_NOFOLLOW
28
+
29
+ const KIND = { MERGE: 'merge', GATE: 'gate', LOAD: 'load' } as const
30
+
31
+ const merge_schema = z.object({ kind: z.literal(KIND.MERGE), at: z.string(), issue: z.number() })
32
+ const gate_schema = z.object({
33
+ kind: z.literal(KIND.GATE),
34
+ at: z.string(),
35
+ elapsed_ms: z.number(),
36
+ is_passed: z.boolean(),
37
+ })
38
+ // `swap_mb` and `lanes` are optional because either read can fail on its own — a platform with no
39
+ // swap reading, a `git worktree` call that did not answer — and a sample is still worth its load.
40
+ const load_schema = z.object({
41
+ kind: z.literal(KIND.LOAD),
42
+ at: z.string(),
43
+ load: z.number(),
44
+ free_mb: z.number(),
45
+ swap_mb: z.number().optional(),
46
+ lanes: z.number().optional(),
47
+ })
48
+ const entry_schema = z.discriminatedUnion('kind', [merge_schema, gate_schema, load_schema])
49
+
50
+ type LedgerEntry = z.infer<typeof entry_schema>
51
+ type MergeEntry = z.infer<typeof merge_schema>
52
+ type GateEntry = z.infer<typeof gate_schema>
53
+ type LoadEntry = z.infer<typeof load_schema>
54
+
55
+ function target_of(repository: string): string {
56
+ return stamp_file.stamp_path(LEDGER_PREFIX, repository, LEDGER_SUFFIX)
57
+ }
58
+
59
+ // The ledger of the repository this checkout belongs to, or `undefined` outside one. `git rev-parse`
60
+ // rejects outside a repository, and `josh gate` resolves this before it runs a check, so the rejection
61
+ // is read as "no ledger" rather than allowed to fail the gate it would only have measured.
62
+ async function target(): Promise<string | undefined> {
63
+ try {
64
+ const repository = await run_carry.repository_directory()
65
+
66
+ return repository === undefined ? undefined : target_of(repository)
67
+ } catch {
68
+ return undefined
69
+ }
70
+ }
71
+
72
+ // **Append, never rewrite** — `O_APPEND` keeps each short line whole across concurrent writers, and
73
+ // `O_NOFOLLOW` plus `stamp_file`'s ownership test keep a planted link or another account's file from
74
+ // receiving it.
75
+ function append(path: string, entry: LedgerEntry): void {
76
+ const descriptor = openSync(path, APPEND_FLAGS, LEDGER_FILE_MODE)
77
+
78
+ try {
79
+ if (stamp_file.is_own_regular_file(path)) {
80
+ writeSync(descriptor, `${JSON.stringify(entry)}${LINE_SEPARATOR}`)
81
+ }
82
+ } finally {
83
+ closeSync(descriptor)
84
+ }
85
+ }
86
+
87
+ // Best-effort: a measurement must never fail the gate or the merge it measures, so every error — no
88
+ // repository, an unwritable temp root — is dropped.
89
+ async function record(entry: LedgerEntry, path?: string): Promise<void> {
90
+ try {
91
+ const destination = path ?? (await target())
92
+
93
+ if (destination !== undefined) append(destination, entry)
94
+ } catch {
95
+ // Reporting is best-effort: a failed append is dropped rather than raised into the caller's work.
96
+ }
97
+ }
98
+
99
+ function now_iso(): string {
100
+ return new Date().toISOString()
101
+ }
102
+
103
+ // One finished `josh gate`, whether it passed or not — a failing gate cost the lane its time as well.
104
+ async function record_gate(path: string, elapsed_ms: number, is_passed: boolean): Promise<void> {
105
+ await record({ kind: KIND.GATE, at: now_iso(), elapsed_ms, is_passed }, path)
106
+ }
107
+
108
+ async function record_merge(issue: number): Promise<void> {
109
+ await record({ kind: KIND.MERGE, at: now_iso(), issue })
110
+ }
111
+
112
+ function parse_line(line: string): LedgerEntry | undefined {
113
+ return entry_schema.safeParse(json_value.parse_or_undefined(line)).data
114
+ }
115
+
116
+ function is_present(entry: LedgerEntry | undefined): entry is LedgerEntry {
117
+ return entry !== undefined
118
+ }
119
+
120
+ // Every entry, oldest first. A malformed line — a write cut short — is skipped rather than making the
121
+ // whole ledger unreadable, and an absent or unowned file is an empty ledger.
122
+ function read_entries(path: string): ReadonlyArray<LedgerEntry> {
123
+ const raw = stamp_file.read_stamp_text(path)
124
+
125
+ if (raw === undefined) return []
126
+
127
+ return raw
128
+ .split(LINE_SEPARATOR)
129
+ .map((line) => parse_line(line.trim()))
130
+ .filter(is_present)
131
+ }
132
+
133
+ const lane_ledger = {
134
+ KIND,
135
+ append,
136
+ read_entries,
137
+ record,
138
+ record_gate,
139
+ record_merge,
140
+ target,
141
+ target_of,
142
+ }
143
+
144
+ export type { GateEntry, LedgerEntry, LoadEntry, MergeEntry }
145
+ export { lane_ledger }
@@ -59,7 +59,7 @@ function walk_files(directory: string, relative: string): Array<string> {
59
59
  return readdirSync(path.join(directory, relative), { withFileTypes: true }).flatMap((entry) => {
60
60
  const child = relative === '' ? entry.name : `${relative}/${entry.name}`
61
61
 
62
- return entry.isDirectory() ? walk_files(directory, child) : [child]
62
+ return entry.isDirectory() ? walk_files(directory, child) : child
63
63
  })
64
64
  }
65
65