@joshuafolkken/kit 1.1178.0 → 1.1240.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/settings.json +2 -2
  3. package/.claude/skills/dependency-update/SKILL.md +4 -8
  4. package/.claude/skills/epic-commands/SKILL.md +2 -2
  5. package/.claude/skills/epic-commands/epic-bundle.md +1 -1
  6. package/.claude/skills/workflow-commands/SKILL.md +2 -4
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +7 -3
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +23 -2
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +9 -4
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +4 -3
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +21 -13
  13. package/.claude/skills/workflow-commands/chain-rule.md +1 -1
  14. package/.claude/skills/workflow-commands/delegation.md +2 -2
  15. package/.claude/skills/workflow-commands/followup.md +10 -0
  16. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  17. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  18. package/.claude/skills/workflow-commands/issue-scout.md +1 -1
  19. package/.claude/skills/workflow-commands/kickoff.md +2 -1
  20. package/.claude/skills/workflow-commands/latest-gate.md +1 -1
  21. package/.claude/skills/workflow-commands/observation-ledger.md +3 -3
  22. package/.claude/skills/workflow-commands/pre-gate-cut.md +6 -1
  23. package/.claude/skills/workflow-commands/prerequisite.md +2 -2
  24. package/.claude/skills/workflow-commands/progress-watcher.md +1 -1
  25. package/.claude/skills/workflow-commands/working-tree-hold.md +1 -1
  26. package/.codex/hooks.json +2 -2
  27. package/.cursorrules +6 -41
  28. package/.github/actions/setup-node/action.yml +1 -1
  29. package/.github/workflows/auto-tag.yml +3 -4
  30. package/.github/workflows/ci.yml +32 -31
  31. package/.github/workflows/dependabot-auto-merge.yml +13 -6
  32. package/.github/workflows/publish.yml +50 -4
  33. package/.github/workflows/sonar-qube.yml +3 -0
  34. package/.pnpmfile.mjs +4 -3
  35. package/.prettierignore +8 -2
  36. package/CLAUDE.md +18 -20
  37. package/README.md +44 -38
  38. package/SECURITY.md +15 -1
  39. package/dist/CLAUDE.md +18 -20
  40. package/dist/commands/document-section-cli.js +734 -0
  41. package/dist/hooks/audit-provision.js +346 -0
  42. package/dist/hooks/batch-guard.js +5 -4
  43. package/dist/hooks/{chunk-G6KRCKEC.js → chunk-4TZVB44A.js} +496 -378
  44. package/dist/hooks/chunk-5VNDSNG3.js +12 -0
  45. package/dist/hooks/chunk-CDY4NFQG.js +15 -0
  46. package/dist/hooks/chunk-ELCN5LCH.js +135 -0
  47. package/dist/hooks/{chunk-VQLLNFNT.js → chunk-F3ELQ2AF.js} +3 -3
  48. package/dist/hooks/{chunk-I4XSL4TT.js → chunk-GPTFMK5E.js} +1 -1
  49. package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
  50. package/dist/hooks/{chunk-UAGLLIGZ.js → chunk-KHGB5S3Y.js} +7 -6
  51. package/dist/hooks/{chunk-GDS5N2KB.js → chunk-KHX4U3BG.js} +735 -683
  52. package/dist/hooks/{chunk-VESKRQOF.js → chunk-KRBERBBI.js} +3 -3
  53. package/dist/hooks/{chunk-DO6NWWHU.js → chunk-QVSWU24O.js} +4 -4
  54. package/dist/hooks/{chunk-S6X6WYKR.js → chunk-WK2XSHSI.js} +1643 -923
  55. package/dist/hooks/{chunk-ZAL3NRGL.js → chunk-XZYKEM6J.js} +25 -10
  56. package/dist/hooks/{chunk-32GQTL4F.js → chunk-ZQGD4GBY.js} +4 -2
  57. package/dist/hooks/codex-hook-adapter.js +14 -12
  58. package/dist/hooks/duplicate-read-guard.js +6 -5
  59. package/dist/hooks/format-edited.js +4 -3
  60. package/dist/hooks/inputs.json +203 -179
  61. package/dist/hooks/investigation-guard.js +6 -5
  62. package/dist/hooks/pretool-guard.js +12 -10
  63. package/dist/hooks/session-lang.js +2 -4
  64. package/dist/hooks/stop-guard.js +66 -72
  65. package/dist/josh.js +1088 -78
  66. package/dist/version/index.d.ts +7 -2
  67. package/dist/version/index.js +152 -75
  68. package/eslint/base.js +23 -1
  69. package/eslint/rules/code-quality.js +3 -0
  70. package/eslint/rules/early-return-one-liner.js +98 -0
  71. package/eslint/rules/unicorn.js +16 -1
  72. package/package.json +7 -6
  73. package/pnpm-workspace.yaml +9 -4
  74. package/prettier/basic.d.ts +3 -1
  75. package/prettier/index.d.ts +3 -1
  76. package/prettier/static.d.ts +3 -1
  77. package/prompts/coding-standards.md +1 -1
  78. package/prompts/collaboration-workflow/file-edits.md +4 -4
  79. package/prompts/collaboration-workflow/glossary.md +1 -1
  80. package/prompts/collaboration-workflow/issue-template.md +1 -0
  81. package/prompts/collaboration-workflow/operating-rules.md +17 -9
  82. package/prompts/collaboration-workflow/overview.md +2 -2
  83. package/prompts/collaboration-workflow/principles.md +14 -12
  84. package/prompts/collaboration-workflow/report-format.md +1 -1
  85. package/prompts/collaboration-workflow/residency.md +1 -1
  86. package/prompts/collaboration-workflow/rule-delivery.md +6 -6
  87. package/prompts/collaboration-workflow/shell-body.md +6 -6
  88. package/prompts/collaboration-workflow/turn-batching.md +1 -1
  89. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -2
  90. package/prompts/collaboration-workflow/wip-cap.md +10 -10
  91. package/prompts/review-rubric.md +1 -2
  92. package/prompts/review.md +1 -1
  93. package/scripts/agent/agent-headless.ts +29 -0
  94. package/scripts/agent/agent-launch-environment.ts +6 -2
  95. package/scripts/agent/codex-agent-argv.ts +2 -1
  96. package/scripts/auto-ok/auto-ok-cli.ts +1 -1
  97. package/scripts/backlog/backlog-arrival.ts +3 -3
  98. package/scripts/backlog/backlog-budget.ts +1 -1
  99. package/scripts/backlog/backlog-drive-args.ts +135 -0
  100. package/scripts/backlog/backlog-drive-cli.ts +11 -137
  101. package/scripts/backlog/backlog-drive-launch.ts +39 -0
  102. package/scripts/backlog/backlog-drive-named-offer.ts +3 -5
  103. package/scripts/backlog/backlog-drive-named.ts +3 -3
  104. package/scripts/backlog/backlog-drive-offer-argv.ts +1 -1
  105. package/scripts/backlog/backlog-drive-owner.ts +1 -1
  106. package/scripts/backlog/backlog-drive-restore.ts +1 -1
  107. package/scripts/backlog/backlog-drive-retrospective.ts +1 -1
  108. package/scripts/backlog/backlog-drive.ts +1 -1
  109. package/scripts/backlog/backlog-next.ts +16 -5
  110. package/scripts/backlog/backlog-offer-cli.ts +6 -6
  111. package/scripts/backlog/backlog-offer.ts +1 -1
  112. package/scripts/backlog/backlog-plan-cli.ts +1 -1
  113. package/scripts/backlog/backlog-pool.ts +57 -11
  114. package/scripts/backlog/backlog-ready.ts +1 -1
  115. package/scripts/backlog/backlog-scope.ts +1 -1
  116. package/scripts/backlog/backlog-stalled-detect.ts +2 -2
  117. package/scripts/backlog/backlog-stalled.ts +1 -1
  118. package/scripts/build/build-codex-hooks.ts +3 -1
  119. package/scripts/build/build-commands.ts +66 -0
  120. package/scripts/build/build-hooks.ts +2 -1
  121. package/scripts/build/build-library.ts +12 -3
  122. package/scripts/build/build.ts +70 -0
  123. package/scripts/build/local-bin.ts +3 -13
  124. package/scripts/delegation/investigation-reads.ts +3 -1
  125. package/scripts/discovery/repo-origin.ts +5 -5
  126. package/scripts/doctor/doctor-consumer.ts +2 -2
  127. package/scripts/doctor/doctor-io.ts +2 -3
  128. package/scripts/document/capped-print-part.ts +30 -0
  129. package/scripts/document/capped-print.ts +3 -4
  130. package/scripts/document/document-byte-budget.ts +11 -2
  131. package/scripts/document/document-scan.ts +117 -0
  132. package/scripts/document/document-section.ts +143 -16
  133. package/scripts/document/entry-read-budget.ts +5 -3
  134. package/scripts/document/entry-read-set.ts +10 -89
  135. package/scripts/document/read-set-cost.ts +98 -0
  136. package/scripts/epic/epic-add-body.ts +1 -0
  137. package/scripts/epic/epic-audit-checks.ts +1 -1
  138. package/scripts/epic/epic-audit-rationale.ts +4 -3
  139. package/scripts/epic/epic-bundle-cli.ts +14 -3
  140. package/scripts/epic/epic-bundle-poll.ts +41 -0
  141. package/scripts/epic/epic-busy.ts +1 -1
  142. package/scripts/epic/epic-candidate-confirm.ts +3 -2
  143. package/scripts/epic/epic-classify.ts +1 -1
  144. package/scripts/epic/epic-cli-argv.ts +78 -46
  145. package/scripts/epic/epic-cli-remove.ts +11 -19
  146. package/scripts/epic/epic-cli.ts +74 -73
  147. package/scripts/epic/epic-close-comment.ts +1 -1
  148. package/scripts/epic/epic-close.ts +1 -1
  149. package/scripts/epic/epic-cross-repo.ts +5 -5
  150. package/scripts/epic/epic-graph.ts +1 -1
  151. package/scripts/epic/epic-index.ts +1 -1
  152. package/scripts/epic/epic-issue.ts +2 -6
  153. package/scripts/epic/epic-lane-offer.ts +2 -2
  154. package/scripts/epic/epic-next.ts +29 -42
  155. package/scripts/epic/epic-solo-stale.ts +1 -1
  156. package/scripts/epic/epic-triage.ts +8 -4
  157. package/scripts/epic/epic-validate.ts +1 -1
  158. package/scripts/followup/git-followup-finish.ts +2 -2
  159. package/scripts/followup/git-followup-label.ts +2 -2
  160. package/scripts/followup/git-followup-merged.ts +2 -2
  161. package/scripts/followup/git-pr-followup-checks.ts +142 -0
  162. package/scripts/followup/git-pr-followup.ts +4 -129
  163. package/scripts/gate/core-admission.ts +162 -0
  164. package/scripts/gate/core-budget.ts +190 -68
  165. package/scripts/gate/gate-plan.ts +80 -7
  166. package/scripts/gate/gate-report.ts +28 -9
  167. package/scripts/gate/gate-skip.ts +38 -7
  168. package/scripts/gate/hook-gate-reuse.ts +12 -10
  169. package/scripts/gate/machine-capacity.ts +175 -0
  170. package/scripts/gate/pre-commit-type-check.ts +4 -1
  171. package/scripts/gate/project-checks.ts +6 -9
  172. package/scripts/gate/type-check-step.ts +2 -3
  173. package/scripts/gate/verification-gate.ts +34 -10
  174. package/scripts/gh/git-gh-api-path.ts +2 -2
  175. package/scripts/gh/git-gh-issue-rest.ts +3 -3
  176. package/scripts/gh/git-gh-issue-write.ts +53 -7
  177. package/scripts/gh/git-gh-pr-read.ts +64 -19
  178. package/scripts/gh/git-gh-pr-rest.ts +1 -1
  179. package/scripts/gh/git-gh-pr.ts +5 -1
  180. package/scripts/gh/git-pr-ai-review.ts +1 -1
  181. package/scripts/gh/git-pr-checks-eval.ts +1 -3
  182. package/scripts/gh/git-pr-checks-parse.ts +1 -1
  183. package/scripts/gh/git-pr-checks-watch.ts +6 -5
  184. package/scripts/gh/git-pr-checks.ts +7 -5
  185. package/scripts/gh/git-pr-coderabbit.ts +1 -1
  186. package/scripts/gh/git-pr-error.ts +1 -3
  187. package/scripts/gh/git-pr.ts +9 -13
  188. package/scripts/gh/github-issue-url.ts +2 -2
  189. package/scripts/git/constants.ts +2 -6
  190. package/scripts/git/git-command.ts +20 -218
  191. package/scripts/git/git-diff-reads.ts +225 -0
  192. package/scripts/git/git-error.ts +4 -12
  193. package/scripts/git/git-pre-push-hook.ts +139 -0
  194. package/scripts/git/git-prompt.ts +2 -6
  195. package/scripts/git/git-spawn-sync.ts +3 -1
  196. package/scripts/git/git-spawn.ts +1 -1
  197. package/scripts/git/git-staging.ts +4 -6
  198. package/scripts/git/git-status.ts +3 -9
  199. package/scripts/hooks/codex-hook-adapter.ts +1 -1
  200. package/scripts/hooks/edited-cspell.ts +2 -4
  201. package/scripts/hooks/format-edited-file.ts +3 -12
  202. package/scripts/hooks/pre-push-unit.ts +31 -9
  203. package/scripts/hooks/reserved-run.ts +2 -2
  204. package/scripts/hooks/run-hook.sh +5 -4
  205. package/scripts/hooks/stop-guard.ts +5 -3
  206. package/scripts/init/init-actions.ts +0 -5
  207. package/scripts/init/init-ai-copy.ts +13 -2
  208. package/scripts/init/init-logic-json-merge.ts +19 -0
  209. package/scripts/init/init-logic-secretlint.ts +7 -7
  210. package/scripts/init/init-logic-workspace.ts +41 -2
  211. package/scripts/init/init-logic.ts +16 -26
  212. package/scripts/init/init.ts +10 -16
  213. package/scripts/init/initial-commit.ts +4 -4
  214. package/scripts/init/kit-development-versions.ts +40 -0
  215. package/scripts/init/kit-setup-state.ts +2 -2
  216. package/scripts/init/package-manager-pin.ts +35 -0
  217. package/scripts/init/start-exec.ts +68 -19
  218. package/scripts/init/start-setup-pr.ts +9 -9
  219. package/scripts/init/start-steps.ts +6 -10
  220. package/scripts/issue/defect-rate-cli.ts +1 -1
  221. package/scripts/issue/git-next-issues.ts +1 -1
  222. package/scripts/issue/issue-auto-ok.ts +32 -7
  223. package/scripts/issue/issue-backlinks-cli.ts +2 -2
  224. package/scripts/issue/issue-backlinks.ts +9 -9
  225. package/scripts/issue/issue-comment-cli.ts +19 -13
  226. package/scripts/issue/issue-file-cli.ts +72 -22
  227. package/scripts/issue/issue-file.ts +29 -1
  228. package/scripts/issue/issue-labels.ts +13 -0
  229. package/scripts/issue/issue-release-cli.ts +155 -0
  230. package/scripts/issue/issue-release.ts +58 -0
  231. package/scripts/issue/issue-scout-closed.ts +1 -1
  232. package/scripts/issue/issue-state-cli.ts +30 -74
  233. package/scripts/josh/josh-command-map.ts +2 -0
  234. package/scripts/josh/josh-command-types.ts +14 -0
  235. package/scripts/josh/josh-commands-ai.ts +4 -4
  236. package/scripts/josh/josh-commands-backlog.ts +4 -1
  237. package/scripts/josh/josh-commands-development.ts +18 -0
  238. package/scripts/josh/josh-commands-document.ts +1 -0
  239. package/scripts/josh/josh-commands-issue.ts +8 -1
  240. package/scripts/josh/josh-commands-lane.ts +24 -1
  241. package/scripts/josh/josh-commands-maintenance.ts +6 -6
  242. package/scripts/josh/josh-commands-run.ts +12 -7
  243. package/scripts/josh/josh-in-process.ts +28 -9
  244. package/scripts/josh/josh-logic.ts +34 -15
  245. package/scripts/josh/process-identity.ts +1 -3
  246. package/scripts/josh/session-language-cli.ts +3 -4
  247. package/scripts/lane/lane-await.ts +5 -1
  248. package/scripts/lane/lane-child-invocation.ts +2 -2
  249. package/scripts/lane/lane-cli.ts +12 -4
  250. package/scripts/lane/lane-dispatch.ts +14 -10
  251. package/scripts/lane/lane-environment.ts +5 -1
  252. package/scripts/lane/lane-handoff.ts +1 -1
  253. package/scripts/lane/lane-install.ts +2 -13
  254. package/scripts/lane/lane-launch-cli.ts +15 -1
  255. package/scripts/lane/lane-ledger.ts +145 -0
  256. package/scripts/lane/lane-leftover.ts +44 -30
  257. package/scripts/lane/lane-load.ts +108 -0
  258. package/scripts/lane/lane-measure-cli.ts +149 -0
  259. package/scripts/lane/lane-occupancy.ts +3 -0
  260. package/scripts/lane/lane-open.ts +5 -5
  261. package/scripts/lane/lane-paths.ts +13 -0
  262. package/scripts/lane/lane-reap.ts +1 -3
  263. package/scripts/lane/lane-registry.ts +11 -0
  264. package/scripts/lane/lane-relaunch.ts +1 -1
  265. package/scripts/lane/lane-stats.ts +224 -0
  266. package/scripts/lane/lane-stray.ts +140 -0
  267. package/scripts/lane/lane-vacant.ts +36 -0
  268. package/scripts/lane/openai-lane-ship.ts +2 -2
  269. package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
  270. package/scripts/lane/openai-lane-supervisor.ts +7 -165
  271. package/scripts/lane/openai-review-broker.ts +2 -4
  272. package/scripts/lib/ancestor-directories.ts +35 -0
  273. package/scripts/lib/animation-helpers.ts +2 -2
  274. package/scripts/lib/buffered-process.ts +3 -7
  275. package/scripts/lib/cli-flags.ts +71 -1
  276. package/scripts/lib/spawn-exit.ts +2 -2
  277. package/scripts/lib/timeouts.ts +35 -2
  278. package/scripts/lines/line-budget.ts +4 -6
  279. package/scripts/notify/telegram-test.ts +6 -0
  280. package/scripts/observations/observation-ledger-line.ts +6 -6
  281. package/scripts/observations/observation-ledger-migrate.ts +55 -4
  282. package/scripts/observations/observation-ledger.ts +26 -7
  283. package/scripts/overrides/lockfile-overrides.ts +1 -1
  284. package/scripts/overrides/overrides-logic.ts +1 -1
  285. package/scripts/propagate/propagate-git.ts +1 -1
  286. package/scripts/propagate/propagate-publish.ts +2 -2
  287. package/scripts/propagate/propagate-steps.ts +20 -15
  288. package/scripts/propagate/propagate.ts +36 -27
  289. package/scripts/refactor/import-graph.ts +18 -1
  290. package/scripts/refactor/refactor-lint.ts +4 -4
  291. package/scripts/release/github-release-environment.ts +1 -0
  292. package/scripts/release/github-release-runs.ts +115 -0
  293. package/scripts/release/github-release.ts +13 -67
  294. package/scripts/repo/repository-labels.ts +2 -2
  295. package/scripts/repo/required-checks-logic.ts +1 -3
  296. package/scripts/review/live-evidence.ts +2 -4
  297. package/scripts/review/review-brief-cli.ts +1 -1
  298. package/scripts/review/review-finding-ledger.ts +1 -1
  299. package/scripts/review/review-findings-cli.ts +1 -3
  300. package/scripts/rules/decision-oracle-batch.ts +128 -0
  301. package/scripts/rules/decision-oracle-tokens.ts +21 -0
  302. package/scripts/rules/decision-oracle.ts +8 -120
  303. package/scripts/rules/delivered-rules.ts +26 -182
  304. package/scripts/rules/direct-filing.ts +1 -1
  305. package/scripts/rules/early-heartbeat.ts +3 -3
  306. package/scripts/rules/git-argv.ts +1 -1
  307. package/scripts/rules/git-force.ts +1 -1
  308. package/scripts/rules/implementation-cut.ts +2 -2
  309. package/scripts/rules/index-guard.ts +1 -1
  310. package/scripts/rules/issue-comments.ts +118 -0
  311. package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
  312. package/scripts/rules/lane-background.ts +2 -2
  313. package/scripts/rules/lane-park.ts +18 -0
  314. package/scripts/rules/lane-split-park.ts +128 -0
  315. package/scripts/rules/oracle-firing.ts +1 -1
  316. package/scripts/rules/poll-loop.ts +1 -1
  317. package/scripts/rules/pre-gate-cut.ts +1 -1
  318. package/scripts/rules/protected-files.ts +59 -4
  319. package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
  320. package/scripts/rules/shell-body-trigger.ts +1 -1
  321. package/scripts/rules/stop-rules.ts +18 -7
  322. package/scripts/rules/test-declared-commit.ts +4 -4
  323. package/scripts/rules/wip-cap.ts +63 -0
  324. package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
  325. package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
  326. package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
  327. package/scripts/run/{run-carry.ts → carry/run-carry.ts} +1 -1
  328. package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
  329. package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +18 -5
  330. package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
  331. package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
  332. package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
  333. package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +1 -0
  334. package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
  335. package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
  336. package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +20 -2
  337. package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
  338. package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
  339. package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
  340. package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
  341. package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
  342. package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
  343. package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
  344. package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
  345. package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
  346. package/scripts/run/run-ending.ts +2 -2
  347. package/scripts/run/run-halfrun-resume.ts +6 -1
  348. package/scripts/run/run-headless.ts +1 -1
  349. package/scripts/run/run-invocation.ts +85 -84
  350. package/scripts/run/run-liveness.ts +1 -1
  351. package/scripts/run/run-parent-cut-hook.ts +1 -1
  352. package/scripts/run/run-preflight.ts +6 -6
  353. package/scripts/run/run-prep-cli.ts +1 -1
  354. package/scripts/run/run-prrun-resume.ts +1 -1
  355. package/scripts/run/run-report-cli.ts +4 -4
  356. package/scripts/run/run-report.ts +2 -2
  357. package/scripts/run/run-status-cli.ts +1 -1
  358. package/scripts/run/run-step-cli.ts +3 -3
  359. package/scripts/run/run-step.ts +7 -4
  360. package/scripts/run/run-stop-notify.ts +1 -1
  361. package/scripts/run/run-stranded-detect.ts +4 -4
  362. package/scripts/run/run-stranded.ts +2 -2
  363. package/scripts/run/run-watcher-guard-cli.ts +1 -1
  364. package/scripts/run/run-watcher-guard.ts +5 -3
  365. package/scripts/run/run-watcher-hook.ts +1 -1
  366. package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +2 -2
  367. package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
  368. package/scripts/run/ship/run-ship-lock.ts +50 -0
  369. package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
  370. package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
  371. package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +4 -3
  372. package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
  373. package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
  374. package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +11 -12
  375. package/scripts/run/ship/run-ship-stop-text.ts +13 -0
  376. package/scripts/run/{run-ship-sync.ts → ship/run-ship-sync.ts} +25 -11
  377. package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
  378. package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +25 -11
  379. package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +58 -7
  380. package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +59 -8
  381. package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
  382. package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +13 -15
  383. package/scripts/run/wake/run-wake-restart.ts +58 -0
  384. package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
  385. package/scripts/run/{run-wake.ts → wake/run-wake.ts} +16 -65
  386. package/scripts/safe-chain/preinstall-command.ts +20 -21
  387. package/scripts/safe-chain/project-config.ts +19 -15
  388. package/scripts/sync/sync-ai-files.ts +1 -1
  389. package/scripts/sync/sync-configs.ts +2 -1
  390. package/scripts/sync/sync.ts +15 -2
  391. package/scripts/test/unit-worker-share.ts +1 -0
  392. package/scripts/time-runtime/time-background.ts +5 -1
  393. package/scripts/time-runtime/time-instant.ts +3 -1
  394. package/scripts/time-runtime/time-spans.ts +6 -139
  395. package/scripts/time-runtime/time-tool-call.ts +160 -0
  396. package/scripts/version/effective-upstream.ts +2 -20
  397. package/scripts/version/latest-corepack.ts +124 -93
  398. package/scripts/version/latest-regression.ts +1 -1
  399. package/scripts/version/latest-update.ts +4 -4
  400. package/scripts/version/npm-registry.ts +20 -5
  401. package/scripts/version/publishable-range-check.ts +2 -2
  402. package/scripts/version/publishable-range.ts +1 -1
  403. package/scripts/version/release-age.ts +48 -38
  404. package/scripts/version/release-hold.ts +1 -1
  405. package/scripts/version/safe-chain-version-update.ts +40 -0
  406. package/scripts/version/upgrade-shell-command.ts +48 -8
  407. package/scripts/version/version-check-logic.ts +23 -14
  408. package/scripts/version/version-commands.ts +23 -11
  409. package/scripts/version/version-remote.ts +28 -13
  410. package/templates/pnpm-workspace.basic.yaml +4 -0
  411. package/templates/workflows/dependabot-auto-merge.yml +5 -5
  412. package/templates/workflows/github-release.yml +6 -0
  413. package/scripts/build/build-config-merge.ts +0 -27
  414. package/scripts/build/build-managed-marker.ts +0 -27
  415. package/scripts/build/build-self-sync-guard.ts +0 -27
  416. package/scripts/build/build-version.ts +0 -26
  417. package/scripts/version/preinstall-version-update.ts +0 -86
  418. /package/scripts/git/{schemas.ts → git-schemas.ts} +0 -0
  419. /package/scripts/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
  420. /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
  421. /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
  422. /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
  423. /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
  424. /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
  425. /package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +0 -0
  426. /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
  427. /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
  428. /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
  429. /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
  430. /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
  431. /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
  432. /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
  433. /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
  434. /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
  435. /package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +0 -0
  436. /package/scripts/run/{run-ship-pre-detach.ts → ship/run-ship-pre-detach.ts} +0 -0
  437. /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
  438. /package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +0 -0
  439. /package/scripts/run/{run-ship.ts → ship/run-ship.ts} +0 -0
  440. /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
  441. /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
  442. /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env tsx
2
+ import { setInterval as every } from 'node:timers/promises'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { cli_flags } from '#scripts/lib/cli-flags'
5
+ import { lane_capacity } from './lane-capacity'
6
+ import { lane_ledger } from './lane-ledger'
7
+ import { lane_load } from './lane-load'
8
+ import { lane_stats } from './lane-stats'
9
+
10
+ // `josh lane:sample` / `josh lane:stats` — the lane-limit measurement (joshuafolkken/kit#3355).
11
+ //
12
+ // `lane:sample` appends one machine-load sample to the ledger, or one every `--every <seconds>` until it
13
+ // is stopped; `lane:stats` reduces a period of the ledger to the one table row #3347 records per lane
14
+ // limit. The merges and the gates write themselves (`run:merge`, `josh gate`), so a person runs only
15
+ // the sampler beside a `backlogrun` and closes the period with `lane:stats`. The procedure is
16
+ // `docs/maintainers/lane-limit-measurement.md`.
17
+
18
+ const ARGV_OFFSET = 2
19
+ const FAILURE_EXIT_CODE = 1
20
+ const MS_PER_SECOND = 1000
21
+ const SAMPLE_VERB = 'sample'
22
+ const STATS_VERB = 'stats'
23
+ const USAGE = [
24
+ 'Usage: josh lane:sample [--every <seconds>]',
25
+ ' josh lane:stats --period <days> [--limit <lane-limit>]',
26
+ ].join('\n')
27
+
28
+ const SAMPLE_OPTIONS = { every: { type: 'string' } } as const
29
+ const STATS_OPTIONS = { period: { type: 'string' }, limit: { type: 'string' } } as const
30
+
31
+ // Where the ledger is and what "now" is, overridable so a test reads a fixed ledger at a fixed instant.
32
+ interface MeasureContext {
33
+ ledger_path?: string
34
+ now_ms?: number
35
+ }
36
+
37
+ function refuse(): number {
38
+ console.error(USAGE)
39
+
40
+ return FAILURE_EXIT_CODE
41
+ }
42
+
43
+ // A positive finite number, or `undefined` for anything else — a typo must not become a zero period.
44
+ function positive(text: string | undefined): number | undefined {
45
+ const value = Number(text)
46
+
47
+ return text !== undefined && Number.isFinite(value) && value > 0 ? value : undefined
48
+ }
49
+
50
+ async function take_sample(context: MeasureContext): Promise<void> {
51
+ const at = new Date(context.now_ms ?? Date.now()).toISOString()
52
+
53
+ await lane_ledger.record(await lane_load.sample(at), context.ledger_path)
54
+ }
55
+
56
+ // The `--every` interval: `{ seconds: undefined }` for a single sample, `undefined` for a refusal.
57
+ function sample_interval(rest: ReadonlyArray<string>): { seconds: number | undefined } | undefined {
58
+ const values = cli_flags.values_of(rest, SAMPLE_OPTIONS)
59
+
60
+ if (values === undefined) return undefined
61
+
62
+ if (values.every === undefined) return { seconds: undefined }
63
+
64
+ const seconds = positive(values.every)
65
+
66
+ return seconds === undefined ? undefined : { seconds }
67
+ }
68
+
69
+ // A sample every `seconds` until a person stops the process, which is how a measurement period is held
70
+ // open; it never returns.
71
+ async function keep_sampling(context: MeasureContext, seconds: number): Promise<void> {
72
+ const ticks = every(seconds * MS_PER_SECOND, context)
73
+
74
+ for await (const tick_context of ticks) await take_sample(tick_context)
75
+ }
76
+
77
+ // One sample now, then — with `--every` — one per interval.
78
+ async function sample_command(
79
+ rest: ReadonlyArray<string>,
80
+ context: MeasureContext,
81
+ ): Promise<number> {
82
+ const interval = sample_interval(rest)
83
+
84
+ if (interval === undefined) return refuse()
85
+
86
+ await take_sample(context)
87
+
88
+ if (interval.seconds !== undefined) await keep_sampling(context, interval.seconds)
89
+
90
+ return 0
91
+ }
92
+
93
+ // The label defaults to the limit this process reads, which is the period's limit only when nobody
94
+ // changed it since — `--limit` names it outright.
95
+ function limit_label(given: string | undefined): string {
96
+ if (given !== undefined) return given
97
+
98
+ const choice = lane_capacity.lane_limit()
99
+
100
+ return choice.kind === 'limit' ? String(choice.limit) : lane_stats.MISSING
101
+ }
102
+
103
+ async function read_ledger(
104
+ context: MeasureContext,
105
+ ): Promise<ReturnType<typeof lane_ledger.read_entries>> {
106
+ const path = context.ledger_path ?? (await lane_ledger.target())
107
+
108
+ return path === undefined ? [] : lane_ledger.read_entries(path)
109
+ }
110
+
111
+ async function stats_command(
112
+ rest: ReadonlyArray<string>,
113
+ context: MeasureContext,
114
+ ): Promise<number> {
115
+ const values = cli_flags.values_of(rest, STATS_OPTIONS)
116
+ const period_days = positive(values?.period)
117
+
118
+ if (values === undefined || period_days === undefined) return refuse()
119
+
120
+ const window = lane_stats.window_of(period_days, context.now_ms ?? Date.now())
121
+ const label = { limit: limit_label(values.limit), period_days }
122
+ const row = lane_stats.row(await read_ledger(context), window, label)
123
+
124
+ console.info(`${lane_stats.header()}\n${row}`)
125
+
126
+ return 0
127
+ }
128
+
129
+ async function run(argv: ReadonlyArray<string>, context: MeasureContext = {}): Promise<number> {
130
+ const [verb, ...rest] = argv
131
+
132
+ if (verb === SAMPLE_VERB) return await sample_command(rest, context)
133
+
134
+ return verb === STATS_VERB ? await stats_command(rest, context) : refuse()
135
+ }
136
+
137
+ async function main(argv: ReadonlyArray<string>): Promise<void> {
138
+ process.exitCode = await run(argv)
139
+ }
140
+
141
+ const lane_measure_cli = {
142
+ USAGE,
143
+ run,
144
+ }
145
+
146
+ if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
147
+
148
+ export type { MeasureContext }
149
+ export { lane_measure_cli }
@@ -82,7 +82,10 @@ function without_label_line(issue: number): string {
82
82
 
83
83
  function labelled_lines(labelled: ReadonlyArray<LabelledIssue>): Array<string> {
84
84
  return labelled.flatMap((entry) => {
85
+ // One return type across the branches (sonarjs/function-return-type) keeps the array wrappers.
86
+ // eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- see above
85
87
  if (entry.liveness === 'stopped') return [stopped_line(entry.issue)]
88
+ // eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- see above
86
89
  if (entry.liveness === 'unknown') return [unknown_line(entry.issue)]
87
90
 
88
91
  return []
@@ -37,14 +37,13 @@ type OpenOutcome =
37
37
 
38
38
  const FULL_OUTCOME: OpenOutcome = { kind: 'full' }
39
39
 
40
- // The seat a lane will take is claimed by exclusively creating its lock directory here, under the
41
- // lanes root and clear of the lanes themselves (which are numbered, never dot-prefixed). The claim
40
+ // The seat a lane will take is claimed by exclusively creating its lock directory under
41
+ // `lane_paths.SEAT_LOCK_DIR`, in the lanes root and clear of the lanes themselves (which are numbered, never dot-prefixed). The claim
42
42
  // is what closes the gap between reading the free seats and writing the lane's `.env`: two
43
43
  // `lane:open` runs that both saw seat 1 free cannot both create the same lock, so the loser steps to
44
44
  // the next free seat (joshuafolkken/kit#1494). The lock is released once the lane's `.env` is on
45
45
  // disk and the seat is discoverable — or on any failure — so only a hard crash mid-open can strand
46
46
  // one, which removing the lanes-root `.seat-locks` directory clears.
47
- const SEAT_LOCK_DIR = '.seat-locks'
48
47
  const SEAT_LOCK_PREFIX = 'seat-'
49
48
 
50
49
  // A root with no `.env` is the normal state of a fresh clone and of CI, and it means seed 0 — the
@@ -79,7 +78,7 @@ function build_lane(
79
78
  }
80
79
 
81
80
  function seat_lock_path(root: string, seat: number): string {
82
- return path.join(root, SEAT_LOCK_DIR, `${SEAT_LOCK_PREFIX}${String(seat)}`)
81
+ return path.join(root, lane_paths.SEAT_LOCK_DIR, `${SEAT_LOCK_PREFIX}${String(seat)}`)
83
82
  }
84
83
 
85
84
  // Release the seat lock — idempotent (`force`), so calling it after the `.env` write and again in the
@@ -93,7 +92,7 @@ function release_seat_lock(lock: string): void {
93
92
  // being created. A missing directory is none held — the normal state when no open is running.
94
93
  function open_in_flight_count(root: string): number {
95
94
  try {
96
- return readdirSync(path.join(root, SEAT_LOCK_DIR)).filter((entry) =>
95
+ return readdirSync(path.join(root, lane_paths.SEAT_LOCK_DIR)).filter((entry) =>
97
96
  entry.startsWith(SEAT_LOCK_PREFIX),
98
97
  ).length
99
98
  } catch {
@@ -279,6 +278,7 @@ async function open_lane(issue: string): Promise<OpenOutcome> {
279
278
  }
280
279
 
281
280
  const lane_open = {
281
+ open_in_flight_count,
282
282
  open_lane,
283
283
  }
284
284
 
@@ -82,10 +82,23 @@ function lane_branch(issue: string): string {
82
82
  return `${issue}${LANE_BRANCH_SUFFIX}`
83
83
  }
84
84
 
85
+ // The lanes root's own bookkeeping, beside the lanes and never one of them: `lane:open` claims a seat
86
+ // by creating a lock directory under it (joshuafolkken/kit#1494).
87
+ const SEAT_LOCK_DIR = '.seat-locks'
88
+
89
+ // A name the lane machinery writes into a lanes root: a lane's issue number, or a lanes root of its
90
+ // own — the nested one a lane opened from inside a lane once put there. Anything else in the root
91
+ // was put there by someone else (joshuafolkken/kit#3370).
92
+ function is_lane_entry(name: string): boolean {
93
+ return LANE_ISSUE_PATTERN.test(name) || DEFAULT_LANE_ROOT_PATTERN.test(name)
94
+ }
95
+
85
96
  const lane_paths = {
86
97
  LANE_BRANCH_SUFFIX,
87
98
  LANE_ROOT_KEY,
99
+ SEAT_LOCK_DIR,
88
100
  default_lane_root,
101
+ is_lane_entry,
89
102
  lane_branch,
90
103
  lane_directory,
91
104
  lane_issue_of,
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process'
2
+ import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
2
3
  import { lane_child_invocation } from './lane-child-invocation'
3
4
 
4
5
  // Ending a lane child that did not end itself (joshuafolkken/kit#2421).
@@ -24,9 +25,6 @@ const CHILDREN_OF_FLAG = '-P'
24
25
  const PS_COMMAND = '/bin/ps'
25
26
  const PARENT_FORMAT = ['-o', 'ppid=', '-p']
26
27
  const TERMINATE_SIGNAL = 'SIGTERM'
27
- // A probe that hung would hang the close it runs inside; failing answers "no process", which leaves
28
- // the lane exactly as it was before this module existed.
29
- const PROBE_TIMEOUT_MS = 2000
30
28
  // `0` addresses the caller's own process group and `1` is init — neither is ever a lane child.
31
29
  const LOWEST_CHILD_PID = 2
32
30
 
@@ -217,6 +217,16 @@ async function list_lanes(): Promise<Array<LaneInfo>> {
217
217
  return lanes.toSorted((left, right) => Number(left.issue) - Number(right.issue))
218
218
  }
219
219
 
220
+ /**
221
+ * Every directory git registers as a work tree, lane or not — what a sweep of the lanes root must
222
+ * never touch, including a work tree at a lane's path whose branch does not read as a lane's.
223
+ */
224
+ async function registered_directories(): Promise<Array<string>> {
225
+ const blocks = await worktree_blocks()
226
+
227
+ return blocks.flatMap((block) => line_value(block.split('\n'), WORKTREE_PREFIX) ?? [])
228
+ }
229
+
220
230
  // The lanes a parent is still waiting on. A stranded lane has no child working in it, so it is not
221
231
  // in-flight — the one definition the watcher guard and the headless stop rule both read.
222
232
  async function has_lanes_in_flight(): Promise<boolean> {
@@ -259,6 +269,7 @@ const lane_registry = {
259
269
  main_repository_root,
260
270
  parse_block,
261
271
  read_environment,
272
+ registered_directories,
262
273
  unreadable_lanes,
263
274
  used_seats,
264
275
  }
@@ -1,6 +1,6 @@
1
1
  import { agent_argv } from '#scripts/agent/agent-argv'
2
2
  import { detached_launch, type LaunchResult } from '#scripts/run/detached-launch'
3
- import { run_ship_detach } from '#scripts/run/run-ship-detach'
3
+ import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
4
4
  import { lane_child_invocation } from './lane-child-invocation'
5
5
  import { lane_child_marker } from './lane-child-marker'
6
6
  import { lane_dispatch_log } from './lane-dispatch-log'
@@ -0,0 +1,224 @@
1
+ import type { GateEntry, LedgerEntry, LoadEntry } from './lane-ledger'
2
+
3
+ // The lane-limit measurement's one table row (joshuafolkken/kit#3355): the ledger entries of one period
4
+ // reduced to throughput, gate duration and machine load, in the column order
5
+ // `docs/maintainers/lane-limit-measurement.md` documents, so the row pastes into #3347's table as is.
6
+ //
7
+ // **A column with no samples prints `—`, never `0`.** A period nobody ran the sampler through has no
8
+ // load, which is not a load of zero; a zero would be read as an idle machine.
9
+ //
10
+ // **Throughput is per active hour** — the time load samples saw a lane child working — rather than per
11
+ // calendar hour, so the nights between runs in a several-day period do not dilute the rate. The gaps
12
+ // between work entries cannot stand in for it: at a low limit the gap from one merge to the next child's
13
+ // first gate is a whole implementation, which a gap cap drops, inflating exactly the low limits' rate.
14
+ // Only a period no sample saw working falls back to the gaps of up to an hour between merges and gates.
15
+ //
16
+ // **The means take working samples only** for the same reason: the sampler runs through the nights,
17
+ // and their idle samples would read four busy lanes as about one. Peaks and minima take every sample.
18
+
19
+ const MS_PER_MINUTE = 60_000
20
+ const MS_PER_HOUR = 3_600_000
21
+ // A busy lane pool finishes a gate well within this; a longer silence is time nobody was running work.
22
+ const ACTIVE_GAP_MS = MS_PER_HOUR
23
+ const MS_PER_DAY = 86_400_000
24
+ const MB_PER_GB = 1024
25
+ const HALF = 2
26
+ const DECIMALS = 1
27
+ const MISSING = '—'
28
+ const CELL_SEPARATOR = ' | '
29
+
30
+ const COLUMNS = [
31
+ 'JOSH_LANE_LIMIT',
32
+ 'period (days)',
33
+ 'merges',
34
+ 'merges / hour',
35
+ 'effective lanes',
36
+ 'gate median (min)',
37
+ 'gate max (min)',
38
+ 'load peak',
39
+ 'load mean',
40
+ 'swap peak (GB)',
41
+ 'swap mean (GB)',
42
+ 'free memory min (GB)',
43
+ 'free memory mean (GB)',
44
+ 'load samples',
45
+ ] as const
46
+
47
+ interface StatsWindow {
48
+ since_ms: number
49
+ until_ms: number
50
+ }
51
+
52
+ interface RowLabel {
53
+ limit: string
54
+ period_days: number
55
+ }
56
+
57
+ function window_of(period_days: number, now_ms: number): StatsWindow {
58
+ return { since_ms: now_ms - period_days * MS_PER_DAY, until_ms: now_ms }
59
+ }
60
+
61
+ function in_window(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow): Array<LedgerEntry> {
62
+ return entries.filter((entry) => {
63
+ const at = Date.parse(entry.at)
64
+
65
+ return at >= window.since_ms && at <= window.until_ms
66
+ })
67
+ }
68
+
69
+ function is_gate(entry: LedgerEntry): entry is GateEntry {
70
+ return entry.kind === 'gate'
71
+ }
72
+
73
+ function is_load(entry: LedgerEntry): entry is LoadEntry {
74
+ return entry.kind === 'load'
75
+ }
76
+
77
+ function is_number(value: number | undefined): value is number {
78
+ return value !== undefined
79
+ }
80
+
81
+ function mean(values: ReadonlyArray<number>): number | undefined {
82
+ if (values.length === 0) return undefined
83
+
84
+ return values.reduce((sum, value) => sum + value, 0) / values.length
85
+ }
86
+
87
+ function median(values: ReadonlyArray<number>): number | undefined {
88
+ if (values.length === 0) return undefined
89
+
90
+ const sorted = values.toSorted((left, right) => left - right)
91
+ const middle = Math.floor(sorted.length / HALF)
92
+
93
+ return sorted.length % HALF === 0 ? mean(sorted.slice(middle - 1, middle + 1)) : sorted[middle]
94
+ }
95
+
96
+ function peak(values: ReadonlyArray<number>): number | undefined {
97
+ return values.length === 0 ? undefined : Math.max(...values)
98
+ }
99
+
100
+ function lowest(values: ReadonlyArray<number>): number | undefined {
101
+ return values.length === 0 ? undefined : Math.min(...values)
102
+ }
103
+
104
+ type WorkingEntry = LoadEntry & { lanes: number }
105
+
106
+ function is_working(sample: LoadEntry): sample is WorkingEntry {
107
+ return (sample.lanes ?? 0) > 0
108
+ }
109
+
110
+ function sum_hours(gaps: ReadonlyArray<number>): number | undefined {
111
+ const active = gaps.filter((gap) => gap <= ACTIVE_GAP_MS).reduce((sum, gap) => sum + gap, 0)
112
+
113
+ return active > 0 ? active / MS_PER_HOUR : undefined
114
+ }
115
+
116
+ // The fallback for a period no sampler saw working: the gaps between consecutive work entries (merges
117
+ // and gates) no longer than `ACTIVE_GAP_MS`.
118
+ function gap_hours(entries: ReadonlyArray<LedgerEntry>): number | undefined {
119
+ const instants = entries
120
+ .filter((entry) => !is_load(entry))
121
+ .map((entry) => Date.parse(entry.at))
122
+ .toSorted((left, right) => left - right)
123
+
124
+ return sum_hours(
125
+ instants.slice(1).map((instant, index) => instant - (instants[index] ?? instant)),
126
+ )
127
+ }
128
+
129
+ // The time from each working sample to the next sample. The cap still applies, so a sampler stopped
130
+ // mid-run does not count its silence as work.
131
+ function sampled_hours(samples: ReadonlyArray<LoadEntry>): number | undefined {
132
+ const sorted = samples.toSorted((left, right) => Date.parse(left.at) - Date.parse(right.at))
133
+ const gaps = sorted
134
+ .slice(1)
135
+ .map((next, index) => ({ sample: sorted[index] ?? next, next }))
136
+ .filter(({ sample }) => is_working(sample))
137
+ .map(({ sample, next }) => Date.parse(next.at) - Date.parse(sample.at))
138
+
139
+ return sum_hours(gaps)
140
+ }
141
+
142
+ // The hours work was under way; `undefined` when nothing leaves any active time.
143
+ function active_hours(entries: ReadonlyArray<LedgerEntry>): number | undefined {
144
+ const samples = entries.filter(is_load)
145
+
146
+ return samples.some((sample) => is_working(sample)) ? sampled_hours(samples) : gap_hours(entries)
147
+ }
148
+
149
+ function gigabytes(megabytes: number | undefined): number | undefined {
150
+ return megabytes === undefined ? undefined : megabytes / MB_PER_GB
151
+ }
152
+
153
+ function cell(value: number | undefined): string {
154
+ return value === undefined ? MISSING : value.toFixed(DECIMALS)
155
+ }
156
+
157
+ function throughput_cells(entries: ReadonlyArray<LedgerEntry>): Array<string> {
158
+ const merges = entries.filter((entry) => entry.kind === 'merge').length
159
+ const hours = active_hours(entries)
160
+ const lanes = entries
161
+ .filter(is_load)
162
+ .filter(is_working)
163
+ .map((entry) => entry.lanes)
164
+ const per_hour = hours === undefined ? undefined : merges / hours
165
+
166
+ return [String(merges), cell(per_hour), cell(mean(lanes))]
167
+ }
168
+
169
+ function gate_cells(entries: ReadonlyArray<LedgerEntry>): Array<string> {
170
+ const minutes = entries.filter(is_gate).map((entry) => entry.elapsed_ms / MS_PER_MINUTE)
171
+
172
+ return [cell(median(minutes)), cell(peak(minutes))]
173
+ }
174
+
175
+ function load_cells(samples: ReadonlyArray<LoadEntry>): Array<string> {
176
+ const working = samples.filter(is_working)
177
+ const swap = samples.map((sample) => sample.swap_mb).filter(is_number)
178
+ const working_swap = working.map((sample) => sample.swap_mb).filter(is_number)
179
+ const free = samples.map((sample) => sample.free_mb)
180
+ const working_free = working.map((sample) => sample.free_mb)
181
+
182
+ return [
183
+ cell(peak(samples.map((sample) => sample.load))),
184
+ cell(mean(working.map((sample) => sample.load))),
185
+ cell(gigabytes(peak(swap))),
186
+ cell(gigabytes(mean(working_swap))),
187
+ cell(gigabytes(lowest(free))),
188
+ cell(gigabytes(mean(working_free))),
189
+ String(samples.length),
190
+ ]
191
+ }
192
+
193
+ function to_row(cells: ReadonlyArray<string>): string {
194
+ return `| ${cells.join(CELL_SEPARATOR)} |`
195
+ }
196
+
197
+ function header(): string {
198
+ return [to_row(COLUMNS), to_row(COLUMNS.map(() => '---'))].join('\n')
199
+ }
200
+
201
+ // The period's row: `entries` is the whole ledger, filtered here to `window`.
202
+ function row(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow, label: RowLabel): string {
203
+ const period = in_window(entries, window)
204
+
205
+ return to_row([
206
+ label.limit,
207
+ String(label.period_days),
208
+ ...throughput_cells(period),
209
+ ...gate_cells(period),
210
+ ...load_cells(period.filter(is_load)),
211
+ ])
212
+ }
213
+
214
+ const lane_stats = {
215
+ COLUMNS,
216
+ MISSING,
217
+ header,
218
+ median,
219
+ row,
220
+ window_of,
221
+ }
222
+
223
+ export type { RowLabel, StatsWindow }
224
+ export { lane_stats }
@@ -0,0 +1,140 @@
1
+ import { existsSync, lstatSync, readdirSync, rmSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { error_text } from '#scripts/lib/error-message'
4
+ import { lane_leftover } from './lane-leftover'
5
+ import { lane_open } from './lane-open'
6
+ import { lane_paths } from './lane-paths'
7
+ import { lane_registry } from './lane-registry'
8
+
9
+ // What the lanes root holds that no registered work tree accounts for (joshuafolkken/kit#3370).
10
+ //
11
+ // **Every other sweep starts from `git worktree list`**, so a directory whose registration is gone —
12
+ // a lane whose `rmSync` stopped half-way, one whose `.git` points at a registration git pruned, an
13
+ // empty lanes root a lane once nested inside itself — is invisible to all of them, and stays on disk
14
+ // for good. This reads the root itself.
15
+ //
16
+ // **A stray is removed only when removing it loses nothing** — `lane_leftover.foreign_paths`, the one
17
+ // test `lane:open`'s reclaim also applies. One that holds anything else is kept and reported with what
18
+ // it holds; so is every entry whose name the lane machinery never writes, because nothing here knows
19
+ // who put it there. The root's own `.seat-locks` and every registered work tree are not strays.
20
+
21
+ type StrayVerdict = 'foreign' | 'not-a-lane' | 'opening' | 'removed' | 'stuck' | 'unreadable'
22
+
23
+ interface Stray {
24
+ path: string
25
+ verdict: StrayVerdict
26
+ // The paths inside it whose removal would lose something — only on `foreign`.
27
+ foreign: Array<string>
28
+ }
29
+
30
+ function stray_names(root: string, registered: ReadonlySet<string>): Array<string> {
31
+ if (!existsSync(root)) return []
32
+
33
+ return readdirSync(root).filter(
34
+ (name) => name !== lane_paths.SEAT_LOCK_DIR && !registered.has(path.join(root, name)),
35
+ )
36
+ }
37
+
38
+ function is_lane_directory(directory: string): boolean {
39
+ return lane_paths.is_lane_entry(path.basename(directory)) && lstatSync(directory).isDirectory()
40
+ }
41
+
42
+ // A removal that stops part-way is judged by what is left, exactly as `lane:close` judges its own —
43
+ // and what is left is a stray the next sweep reads again.
44
+ function remove(directory: string): StrayVerdict {
45
+ try {
46
+ rmSync(directory, { force: true, recursive: true })
47
+ } catch (error) {
48
+ error_text.trace_swallowed('lane_stray.remove', error)
49
+ }
50
+
51
+ return existsSync(directory) ? 'stuck' : 'removed'
52
+ }
53
+
54
+ // **A `lane:open` in flight holds a seat lock from before its `worktree add` until after the
55
+ // registration**, so while any lock is held a numbered directory may be a lane being created, not a
56
+ // leftover — it is kept, and the next sweep reads it again.
57
+ interface SweepContext {
58
+ recoverable: ReadonlySet<string>
59
+ is_opening: boolean
60
+ }
61
+
62
+ function judge(directory: string, context: SweepContext): Stray {
63
+ if (!is_lane_directory(directory)) return { path: directory, verdict: 'not-a-lane', foreign: [] }
64
+ if (context.is_opening) return { path: directory, verdict: 'opening', foreign: [] }
65
+
66
+ const foreign = lane_leftover.foreign_paths(directory, context.recoverable)
67
+
68
+ if (foreign.length > 0) return { path: directory, verdict: 'foreign', foreign }
69
+
70
+ return { path: directory, verdict: remove(directory), foreign: [] }
71
+ }
72
+
73
+ // One stray that cannot be read — a permission, a file vanishing mid-walk — is kept and reported
74
+ // rather than thrown, so it never stops the sweep of the rest.
75
+ function judge_safely(directory: string, context: SweepContext): Stray {
76
+ try {
77
+ return judge(directory, context)
78
+ } catch (error) {
79
+ error_text.trace_swallowed('lane_stray.judge', error)
80
+
81
+ return { path: directory, verdict: 'unreadable', foreign: [] }
82
+ }
83
+ }
84
+
85
+ /** Remove the lanes root's unregistered leftovers that hold nothing worth keeping; report the rest. */
86
+ async function sweep_strays(): Promise<Array<Stray>> {
87
+ const root = lane_paths.lane_root(await lane_registry.main_repository_root())
88
+ const registered = new Set(await lane_registry.registered_directories())
89
+ const names = stray_names(root, registered)
90
+
91
+ if (names.length === 0) return []
92
+
93
+ const context: SweepContext = {
94
+ recoverable: await lane_leftover.recoverable_objects(),
95
+ is_opening: lane_open.open_in_flight_count(root) > 0,
96
+ }
97
+
98
+ return names.map((name) => judge_safely(path.join(root, name), context))
99
+ }
100
+
101
+ // The foreign paths a report names before it summarizes the rest as a count.
102
+ const NAMED_PATH_LIMIT = 5
103
+
104
+ function named_paths(foreign: ReadonlyArray<string>): string {
105
+ const rest = foreign.length - NAMED_PATH_LIMIT
106
+ const more = rest > 0 ? ` and ${String(rest)} more` : ''
107
+
108
+ return `${foreign.slice(0, NAMED_PATH_LIMIT).join(', ')}${more}`
109
+ }
110
+
111
+ const KEPT_REASONS: Record<Exclude<StrayVerdict, 'foreign' | 'removed'>, string> = {
112
+ 'not-a-lane': 'not a name the lane machinery writes, so it was left alone',
113
+ opening:
114
+ 'a `lane:open` holds a seat lock and this may be its lane; the next sweep reads it again — if no `lane:open` is running, the lock outlived a crash: remove the lanes root `.seat-locks` directory',
115
+ stuck: 'the removal did not finish; the next `pnpm josh lane:prune` tries again',
116
+ unreadable: 'it could not be read, so it was left in place',
117
+ }
118
+
119
+ /** One line per stray, for standard error. */
120
+ function describe(stray: Stray): string {
121
+ if (stray.verdict === 'removed') return `Removed unregistered lane leftover ${stray.path}.`
122
+
123
+ if (stray.verdict === 'foreign') {
124
+ return `Kept ${stray.path}: it holds files git cannot restore (${named_paths(stray.foreign)}). Move them out and delete the directory.`
125
+ }
126
+
127
+ return `Kept ${stray.path}: ${KEPT_REASONS[stray.verdict]}.`
128
+ }
129
+
130
+ /** Sweep, and say on standard error what was removed and what was kept. */
131
+ async function sweep_and_report(): Promise<void> {
132
+ const strays = await sweep_strays()
133
+
134
+ for (const stray of strays) console.error(describe(stray))
135
+ }
136
+
137
+ const lane_stray = { describe, sweep_and_report, sweep_strays }
138
+
139
+ export type { Stray, StrayVerdict }
140
+ export { lane_stray }
@@ -0,0 +1,36 @@
1
+ import { git_command } from '#scripts/git/git-command'
2
+ import { run_hold } from '#scripts/run/hold/run-hold'
3
+ import { lane_await } from './lane-await'
4
+ import type { LaneInfo } from './lane-registry'
5
+
6
+ // Whether an open lane holds nothing worth keeping (joshuafolkken/kit#3289): no commit on its branch
7
+ // beyond the default branch, a clean work tree, and no child or ship supervisor alive for its issue. A
8
+ // `backlogrun` cut between `lane:open` and `lane:dispatch` leaves exactly such a lane, and `lane:open`
9
+ // refuses it as `already-open` — so the next launch of that issue failed on a lane with nothing in it.
10
+ //
11
+ // **Every unreadable answer is "not vacant"**: `is_tree_dirty` already reads an unreadable status as
12
+ // dirty, and a branch whose commits cannot be counted is kept rather than closed, because closing it
13
+ // deletes the branch and anything on it.
14
+ async function has_no_commits(lane: LaneInfo): Promise<boolean> {
15
+ try {
16
+ const base = await git_command.default_branch_reference()
17
+
18
+ return (await git_command.commit_count_beyond(base, lane.branch)) === 0
19
+ } catch {
20
+ return false
21
+ }
22
+ }
23
+
24
+ function is_occupied(lane: LaneInfo): boolean {
25
+ return lane.is_stranded || lane_await.is_process_running_default(lane.issue)
26
+ }
27
+
28
+ async function is_vacant(lane: LaneInfo): Promise<boolean> {
29
+ if (is_occupied(lane) || (await run_hold.is_tree_dirty(lane.directory))) return false
30
+
31
+ return await has_no_commits(lane)
32
+ }
33
+
34
+ const lane_vacant = { is_vacant }
35
+
36
+ export { lane_vacant }