@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
@@ -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,7 +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 } from '#scripts/lane/lane-registry'
4
+ import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
5
+ import { lane_vacant } from '#scripts/lane/lane-vacant'
5
6
  import { cli_flags } from '#scripts/lib/cli-flags'
6
7
  import { INSTALL_TIMEOUT_MS } from '#scripts/lib/timeouts'
7
8
  import { execa } from 'execa'
@@ -88,17 +89,30 @@ type LaunchOutcome = { kind: 'launched'; pid: string } | { kind: 'unopened' } |
88
89
  const UNOPENED: LaunchOutcome = { kind: 'unopened' }
89
90
  const FAILED: LaunchOutcome = { kind: 'failed' }
90
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
+
91
98
  // **A lane a park kept open is resumed, not reopened** (joshuafolkken/kit#3228). A child parked before
92
99
  // its commit leaves its uncommitted work in its lane, and `lane:open` refuses that lane as
93
100
  // `already-open` — so the released child is dispatched into the kept tree instead. **Only a lane a
94
101
  // child was already dispatched into counts as kept**: `lane:dispatch` records the child's output in the
95
102
  // lane's `.env`, and a lane whose install failed never got that far, so it still goes through
96
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.
97
109
  async function lane_directory(issue: string): Promise<string | undefined> {
98
110
  const kept = await lane_registry.find_open_lane(issue)
99
111
 
100
112
  if (kept?.output !== undefined) return kept.directory
101
113
 
114
+ await close_if_vacant(kept)
115
+
102
116
  const opened = await josh_command.josh_run(['lane:open', issue], should_forward_stderr)
103
117
 
104
118
  return opened.code === SUCCESS_EXIT_CODE ? opened.out : undefined
@@ -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 }
@@ -1,7 +1,9 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync, rmSync } from 'node:fs'
3
3
  import path from 'node:path'
4
+ import { ENV_FILE_NAME } from '#ports'
4
5
  import { git_spawn } from '#scripts/git/git-spawn'
6
+ import { IGNORED_CACHE_FILES } from '#scripts/josh/josh-command-types'
5
7
 
6
8
  // A lane directory git no longer registers as a work tree (joshuafolkken/kit#2857).
7
9
  //
@@ -11,31 +13,38 @@ import { git_spawn } from '#scripts/git/git-spawn'
11
13
  // here before it adds the tree.
12
14
  //
13
15
  // **The test for removing it is that removing it loses nothing**, not a list of names. Every file in
14
- // it has to be one the repository already holds with the same content, so it is a checkout git would
15
- // write again; the one exception is the `.git` file, a pointer to the registration that is gone. A
16
- // list of generated names would break the next time a file is generated, and would delete whatever
17
- // shares one of those names.
18
-
19
- // `git ls-tree -r -z`: `<mode> <type> <object>\t<path>`, entries separated by NUL — the `-z` form
20
- // leaves a path with an unusual character unquoted, so it can be compared to the path on disk.
21
- const TREE_ENTRY_PATTERN = /^\d+ blob (?<object>[\da-f]+)\t(?<path>.+)$/su
16
+ // it has to hold content the repository's history already stores — a blob some ref reaches — so git
17
+ // can write it again; the one exception is the `.git` file, a pointer to the registration that is
18
+ // gone. **Any commit's content, not only `HEAD`'s at the same path** (joshuafolkken/kit#3370): a
19
+ // leftover is a checkout of whatever commit its lane was on, and measured against `HEAD` the
20
+ // leftovers that issue found differed in 167 to 1,431 files each while every one of them was in the
21
+ // history. A list of generated names would break the next time a file is generated, and would delete
22
+ // whatever shares one of those names.
23
+ //
24
+ // **The one list of names is what `lane:open` and the gate regenerate**, and only at the top level:
25
+ // the install's `node_modules`, the build's `dist`, the `.env` `lane:open` writes from the root's, and
26
+ // the gate's caches. A lane carries them on top of its checkout, none is in the history, and walking
27
+ // `node_modules` alone would hash tens of thousands of files.
28
+ const REGENERATED_PATHS: ReadonlySet<string> = new Set([
29
+ 'node_modules',
30
+ 'dist',
31
+ ENV_FILE_NAME,
32
+ ...IGNORED_CACHE_FILES,
33
+ ])
22
34
  const GIT_POINTER = '.git'
23
35
  const GITDIR_PREFIX = 'gitdir: '
24
36
  // The foreign paths a refusal names before it summarizes the rest as a count.
25
37
  const NAMED_PATH_LIMIT = 10
26
38
 
27
- // One blob entry as `[path, object id]`; a tree, a commit (a submodule) or the trailing empty entry is
28
- // none.
29
- function tree_entry(entry: string): Array<[string, string]> {
30
- const { path: entry_path, object } = TREE_ENTRY_PATTERN.exec(entry)?.groups ?? {}
31
-
32
- if (entry_path === undefined || object === undefined) return []
33
-
34
- return [[entry_path, object]]
39
+ // `git rev-list --objects --all`: one `<object>[ <path>]` line per object any ref reaches — commits,
40
+ // trees and blobs alike, which costs nothing here, because only a blob id can equal a file's.
41
+ function parse_objects(listing: string): Set<string> {
42
+ return new Set(listing.split('\n').map((line) => line.split(' ', 1)[0] ?? ''))
35
43
  }
36
44
 
37
- function parse_tree(listing: string): Map<string, string> {
38
- return new Map(listing.split('\0').flatMap((entry) => tree_entry(entry)))
45
+ /** Every object id the repository's refs reach — the content a leftover can be restored from. */
46
+ async function recoverable_objects(): Promise<Set<string>> {
47
+ return parse_objects(await git_spawn.read(['rev-list', '--objects', '--all']))
39
48
  }
40
49
 
41
50
  // The object id git gives this file — a symbolic link's is that of its target string, as git stores it.
@@ -59,13 +68,18 @@ function walk_files(directory: string, relative: string): Array<string> {
59
68
  return readdirSync(path.join(directory, relative), { withFileTypes: true }).flatMap((entry) => {
60
69
  const child = relative === '' ? entry.name : `${relative}/${entry.name}`
61
70
 
62
- return entry.isDirectory() ? walk_files(directory, child) : [child]
71
+ return entry.isDirectory() ? walk_files(directory, child) : child
63
72
  })
64
73
  }
65
74
 
66
- // Every file under `directory` as a repository path, symbolic links included and never followed.
75
+ // Every file under `directory` as a repository path, symbolic links included and never followed —
76
+ // less the regenerated top-level entries, which are neither walked nor listed.
67
77
  function list_files(directory: string): Array<string> {
68
- return walk_files(directory, '')
78
+ return readdirSync(directory)
79
+ .filter((name) => !REGENERATED_PATHS.has(name))
80
+ .flatMap((name) =>
81
+ lstatSync(path.join(directory, name)).isDirectory() ? walk_files(directory, name) : name,
82
+ )
69
83
  }
70
84
 
71
85
  // The `.git` file is disposable only while the registration it points to is gone: one that still
@@ -85,25 +99,25 @@ function is_dangling_pointer(directory: string): boolean {
85
99
  function is_reclaimable_file(
86
100
  directory: string,
87
101
  relative: string,
88
- tracked: ReadonlyMap<string, string>,
102
+ recoverable: ReadonlySet<string>,
89
103
  ): boolean {
90
104
  if (relative === GIT_POINTER) return is_dangling_pointer(directory)
91
105
 
92
- return tracked.get(relative) === blob_id(path.join(directory, relative))
106
+ return recoverable.has(blob_id(path.join(directory, relative)))
93
107
  }
94
108
 
95
109
  /**
96
110
  * The paths under `directory` whose removal would lose something — empty when it holds nothing but
97
- * the repository's own files and the `.git` pointer. A `.git` directory is a repository of its own,
98
- * so it is foreign as a whole rather than walked.
111
+ * content the history stores, the regenerated entries and the `.git` pointer. A `.git` directory is a
112
+ * repository of its own, so it is foreign as a whole rather than walked.
99
113
  */
100
- function foreign_paths(directory: string, tracked: ReadonlyMap<string, string>): Array<string> {
114
+ function foreign_paths(directory: string, recoverable: ReadonlySet<string>): Array<string> {
101
115
  const pointer = lstatSync(path.join(directory, GIT_POINTER), { throwIfNoEntry: false })
102
116
 
103
117
  if (pointer?.isDirectory() === true) return [GIT_POINTER]
104
118
 
105
119
  return list_files(directory).filter(
106
- (relative) => !is_reclaimable_file(directory, relative, tracked),
120
+ (relative) => !is_reclaimable_file(directory, relative, recoverable),
107
121
  )
108
122
  }
109
123
 
@@ -125,8 +139,7 @@ function refusal(directory: string, foreign: ReadonlyArray<string>): Error {
125
139
  async function reclaim(directory: string): Promise<void> {
126
140
  if (!existsSync(directory)) return
127
141
 
128
- const tracked = parse_tree(await git_spawn.read(['ls-tree', '-r', '-z', '--full-tree', 'HEAD']))
129
- const foreign = foreign_paths(directory, tracked)
142
+ const foreign = foreign_paths(directory, await recoverable_objects())
130
143
 
131
144
  if (foreign.length > 0) throw refusal(directory, foreign)
132
145
 
@@ -136,8 +149,9 @@ async function reclaim(directory: string): Promise<void> {
136
149
  const lane_leftover = {
137
150
  blob_id,
138
151
  foreign_paths,
139
- parse_tree,
152
+ parse_objects,
140
153
  reclaim,
154
+ recoverable_objects,
141
155
  }
142
156
 
143
157
  export { lane_leftover }
@@ -0,0 +1,108 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { freemem, loadavg } from 'node:os'
3
+ import { execa } from 'execa'
4
+ import { lane_await } from './lane-await'
5
+ import type { LoadEntry } from './lane-ledger'
6
+ import { lane_registry, type LaneInfo } from './lane-registry'
7
+
8
+ // One machine-load sample for the lane-limit measurement (joshuafolkken/kit#3355): the one-minute load
9
+ // average, free memory, swap in use and how many lanes have a child working, read at one instant so
10
+ // `josh lane:stats` can set the load beside the lane count that produced it.
11
+ //
12
+ // **Swap is read per platform and is optional.** Node exposes no swap figure, so macOS asks `sysctl` and
13
+ // Linux reads `/proc/meminfo`; any other platform, or an output neither parser recognizes, records the
14
+ // sample without it rather than as a zero nobody measured.
15
+
16
+ const BYTES_PER_MB = 1_048_576
17
+ const MB_PER_GB = 1024
18
+ const KB_PER_MB = 1024
19
+ const DARWIN_SWAP = /used = (?<amount>[\d.]+)(?<unit>[MG])/u
20
+ const LINUX_SWAP_TOTAL = /^SwapTotal:\s+(?<kb>\d+) kB/mu
21
+ const LINUX_SWAP_FREE = /^SwapFree:\s+(?<kb>\d+) kB/mu
22
+ const MEMINFO_PATH = '/proc/meminfo'
23
+ const SYSCTL_ARGUMENTS = ['-n', 'vm.swapusage']
24
+ const GB_UNIT = 'G'
25
+
26
+ // `sysctl -n vm.swapusage` → `total = 5120.00M used = 3993.25M free = 1126.75M (encrypted)`.
27
+ function parse_darwin_swap(output: string): number | undefined {
28
+ const groups = DARWIN_SWAP.exec(output)?.groups
29
+
30
+ if (groups?.['amount'] === undefined) return undefined
31
+
32
+ const scale = groups['unit'] === GB_UNIT ? MB_PER_GB : 1
33
+
34
+ return Number(groups['amount']) * scale
35
+ }
36
+
37
+ function kilobytes_of(meminfo: string, pattern: RegExp): number | undefined {
38
+ const kb = pattern.exec(meminfo)?.groups?.['kb']
39
+
40
+ return kb === undefined ? undefined : Number(kb)
41
+ }
42
+
43
+ // `/proc/meminfo` carries the total and the free swap; what is in use is their difference.
44
+ function parse_linux_swap(meminfo: string): number | undefined {
45
+ const total = kilobytes_of(meminfo, LINUX_SWAP_TOTAL)
46
+ const free = kilobytes_of(meminfo, LINUX_SWAP_FREE)
47
+
48
+ if (total === undefined || free === undefined) return undefined
49
+
50
+ return (total - free) / KB_PER_MB
51
+ }
52
+
53
+ async function read_swap_mb(
54
+ platform: NodeJS.Platform = process.platform,
55
+ ): Promise<number | undefined> {
56
+ try {
57
+ if (platform === 'darwin') {
58
+ const { stdout } = await execa('sysctl', SYSCTL_ARGUMENTS)
59
+
60
+ return parse_darwin_swap(stdout)
61
+ }
62
+
63
+ return platform === 'linux' ? parse_linux_swap(await readFile(MEMINFO_PATH, 'utf8')) : undefined
64
+ } catch {
65
+ return undefined
66
+ }
67
+ }
68
+
69
+ // The lanes with a child at work. An open lane is not enough: a stranded lane, or one a park kept, holds
70
+ // a tree but no running child, so it adds no load — the child's process is what `lane:await` polls.
71
+ function count_working(
72
+ lanes: ReadonlyArray<LaneInfo>,
73
+ is_running: (issue: string) => boolean = lane_await.is_process_running_default,
74
+ ): number {
75
+ return lanes.filter((lane) => !lane.is_stranded && is_running(lane.issue)).length
76
+ }
77
+
78
+ async function read_lanes(): Promise<number | undefined> {
79
+ try {
80
+ return count_working(await lane_registry.list_lanes())
81
+ } catch {
82
+ return undefined
83
+ }
84
+ }
85
+
86
+ async function sample(at: string): Promise<LoadEntry> {
87
+ const [swap_mb, lanes] = await Promise.all([read_swap_mb(), read_lanes()])
88
+ const [load = 0] = loadavg()
89
+
90
+ return {
91
+ kind: 'load',
92
+ at,
93
+ load,
94
+ free_mb: Math.round(freemem() / BYTES_PER_MB),
95
+ swap_mb,
96
+ lanes,
97
+ }
98
+ }
99
+
100
+ const lane_load = {
101
+ count_working,
102
+ parse_darwin_swap,
103
+ parse_linux_swap,
104
+ read_swap_mb,
105
+ sample,
106
+ }
107
+
108
+ export { lane_load }