@joshuafolkken/kit 1.1455.0 → 1.1514.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 (374) hide show
  1. package/.claude/settings.json +6 -6
  2. package/.claude/skills/dependency-update/SKILL.md +26 -55
  3. package/.claude/skills/epic-commands/SKILL.md +83 -156
  4. package/.claude/skills/epic-commands/epic-bundle.md +51 -79
  5. package/.claude/skills/epic-commands/execution-waves.md +43 -81
  6. package/.claude/skills/verify-ui/SKILL.md +24 -50
  7. package/.claude/skills/workflow-commands/SKILL.md +7 -9
  8. package/.claude/skills/workflow-commands/background-commands.md +31 -46
  9. package/.claude/skills/workflow-commands/backlogrun-child.md +65 -85
  10. package/.claude/skills/workflow-commands/backlogrun-lanes.md +86 -125
  11. package/.claude/skills/workflow-commands/backlogrun-park.md +68 -118
  12. package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -28
  13. package/.claude/skills/workflow-commands/backlogrun-recovery.md +17 -26
  14. package/.claude/skills/workflow-commands/backlogrun-steps.md +35 -52
  15. package/.claude/skills/workflow-commands/backlogrun.md +31 -50
  16. package/.claude/skills/workflow-commands/chain-rule.md +31 -40
  17. package/.claude/skills/workflow-commands/delegation.md +12 -8
  18. package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
  19. package/.claude/skills/workflow-commands/followup.md +55 -77
  20. package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
  21. package/.claude/skills/workflow-commands/fullrun.md +4 -6
  22. package/.claude/skills/workflow-commands/halfrun.md +3 -7
  23. package/.claude/skills/workflow-commands/into-target.md +1 -2
  24. package/.claude/skills/workflow-commands/issue-comments.md +43 -60
  25. package/.claude/skills/workflow-commands/issue-scout.md +3 -2
  26. package/.claude/skills/workflow-commands/kickoff.md +65 -74
  27. package/.claude/skills/workflow-commands/latest-gate.md +2 -6
  28. package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
  29. package/.claude/skills/workflow-commands/observation-filing.md +59 -118
  30. package/.claude/skills/workflow-commands/observation-ledger.md +52 -112
  31. package/.claude/skills/workflow-commands/pre-gate-cut.md +88 -142
  32. package/.claude/skills/workflow-commands/prerequisite.md +11 -17
  33. package/.claude/skills/workflow-commands/progress-watcher.md +23 -33
  34. package/.claude/skills/workflow-commands/prrun.md +3 -5
  35. package/.claude/skills/workflow-commands/retrospective.md +35 -54
  36. package/.claude/skills/workflow-commands/split-assessment.md +32 -58
  37. package/.claude/skills/workflow-commands/target-repository.md +1 -2
  38. package/.claude/skills/workflow-commands/working-tree-hold.md +54 -73
  39. package/.codex/hooks.json +6 -6
  40. package/.github/actions/setup-node/action.yml +1 -1
  41. package/.github/workflows/ci.yml +1 -1
  42. package/.github/workflows/publish.yml +2 -2
  43. package/CLAUDE.md +7 -6
  44. package/cspell/index.yaml +7 -0
  45. package/dist/CLAUDE.md +7 -6
  46. package/dist/commands/document-section-cli.js +67 -34
  47. package/dist/hooks/batch-guard.js +6 -6
  48. package/dist/hooks/chunk-4KKD2UTK.js +66 -0
  49. package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-6TS2JD6D.js} +14 -3
  50. package/dist/hooks/chunk-7DSBJVFH.js +50 -0
  51. package/dist/hooks/{chunk-5H4SSDSD.js → chunk-A65Y4YOR.js} +3 -3
  52. package/dist/hooks/{chunk-3C4XRYG6.js → chunk-APUO6U4J.js} +102 -19
  53. package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
  54. package/dist/hooks/chunk-GP4IEC4O.js +20 -0
  55. package/dist/hooks/chunk-JP45SOCJ.js +56 -0
  56. package/dist/hooks/{chunk-OLMO72ON.js → chunk-LAOV7NLM.js} +1 -1
  57. package/dist/hooks/chunk-M4HGZV7U.js +83 -0
  58. package/dist/hooks/{chunk-TBJFTSUY.js → chunk-O6GDRNJB.js} +9 -7
  59. package/dist/hooks/{chunk-HFGWYGUV.js → chunk-PF2NZL2A.js} +5 -5
  60. package/dist/hooks/{chunk-WNLVGT7E.js → chunk-PTICX4OL.js} +4 -5
  61. package/dist/hooks/{chunk-HHI6FO3R.js → chunk-RT27QRJW.js} +763 -1515
  62. package/dist/hooks/{chunk-7WQBPLUN.js → chunk-SYOTDYCI.js} +1149 -1095
  63. package/dist/hooks/{chunk-LIAFCXL7.js → chunk-TNUMY6NL.js} +1519 -1332
  64. package/dist/hooks/chunk-UPUZF3AN.js +279 -0
  65. package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-WK6SMBIC.js} +1932 -784
  66. package/dist/hooks/codex-hook-adapter.js +17 -14
  67. package/dist/hooks/duplicate-read-guard.js +7 -7
  68. package/dist/hooks/format-edited.js +5 -4
  69. package/dist/hooks/inputs.json +302 -247
  70. package/dist/hooks/investigation-guard.js +8 -11
  71. package/dist/hooks/pretool-guard.js +16 -13
  72. package/dist/hooks/session-lang.js +372 -26
  73. package/dist/hooks/stop-guard.js +2593 -189
  74. package/dist/josh.js +2128 -282
  75. package/dist/managed-marker/index.js +1 -2
  76. package/dist/version/index.js +15 -12
  77. package/eslint/base.js +2 -1
  78. package/eslint/rules/unicorn.js +16 -0
  79. package/lefthook/base.yml +5 -3
  80. package/package.json +14 -12
  81. package/prompts/coding-standards.md +12 -22
  82. package/prompts/collaboration-workflow/file-edits.md +25 -45
  83. package/prompts/collaboration-workflow/gh-rest.md +19 -36
  84. package/prompts/collaboration-workflow/glossary.md +5 -7
  85. package/prompts/collaboration-workflow/issue-citation.md +6 -14
  86. package/prompts/collaboration-workflow/issue-template.md +40 -32
  87. package/prompts/collaboration-workflow/lane-instructions.md +35 -0
  88. package/prompts/collaboration-workflow/operating-rules.md +33 -46
  89. package/prompts/collaboration-workflow/output-bounds.md +15 -15
  90. package/prompts/collaboration-workflow/overview.md +14 -19
  91. package/prompts/collaboration-workflow/plan-comment.md +13 -13
  92. package/prompts/collaboration-workflow/principles.md +57 -54
  93. package/prompts/collaboration-workflow/proposal-request.md +1 -9
  94. package/prompts/collaboration-workflow/report-format.md +41 -49
  95. package/prompts/collaboration-workflow/residency.md +15 -11
  96. package/prompts/collaboration-workflow/rule-delivery.md +7 -15
  97. package/prompts/collaboration-workflow/shell-body.md +10 -20
  98. package/prompts/collaboration-workflow/turn-batching.md +8 -14
  99. package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
  100. package/prompts/collaboration-workflow/wip-cap.md +26 -32
  101. package/prompts/collaboration-workflow.md +25 -25
  102. package/prompts/refactoring.md +33 -43
  103. package/prompts/review-rubric.md +84 -129
  104. package/prompts/review.md +110 -141
  105. package/prompts/sonar-hotspot-handling.md +22 -50
  106. package/prompts/testing-guide.md +48 -99
  107. package/scripts/adopt/adopt-toolkits.ts +2 -4
  108. package/scripts/agent/agent-argv.ts +20 -4
  109. package/scripts/agent/agent-exit-record.ts +23 -1
  110. package/scripts/agent/agent-role-profile.ts +101 -45
  111. package/scripts/agent/agent-safe-text.ts +60 -0
  112. package/scripts/agent/claude-agent-argv.ts +14 -0
  113. package/scripts/agent/codex-agent-argv.ts +9 -2
  114. package/scripts/agent/lane-instructions.ts +144 -0
  115. package/scripts/backlog/backlog-budget-cli.ts +1 -2
  116. package/scripts/backlog/backlog-budget.ts +4 -4
  117. package/scripts/backlog/backlog-drive-carried.ts +26 -0
  118. package/scripts/backlog/backlog-drive-cli.ts +10 -5
  119. package/scripts/backlog/backlog-drive-named.ts +3 -5
  120. package/scripts/backlog/backlog-drive-restore.ts +28 -2
  121. package/scripts/backlog/backlog-drive-seats.ts +27 -0
  122. package/scripts/backlog/backlog-drive.ts +11 -6
  123. package/scripts/backlog/backlog-occupied.ts +56 -0
  124. package/scripts/backlog/backlog-ready.ts +59 -15
  125. package/scripts/behavior/behavior-rules.ts +3 -2
  126. package/scripts/cost-runtime/codex-usage.ts +4 -2
  127. package/scripts/cost-runtime/cost-cli.ts +1 -2
  128. package/scripts/delegation/duplicate-read-guard.ts +2 -2
  129. package/scripts/delegation/investigation-guard.ts +15 -65
  130. package/scripts/delegation/investigation-reads.ts +1 -2
  131. package/scripts/discovery/repo-discovery.ts +4 -2
  132. package/scripts/discovery/repo-origin.ts +2 -4
  133. package/scripts/doctor/doctor-logic.ts +6 -3
  134. package/scripts/document/document-byte-budget.ts +28 -27
  135. package/scripts/document/document-byte-check.ts +1 -2
  136. package/scripts/document/document-section-cli.ts +17 -5
  137. package/scripts/document/entry-read-budget.ts +3 -3
  138. package/scripts/document/read-set-cli.ts +4 -1
  139. package/scripts/epic/epic-audit-orphans.ts +5 -8
  140. package/scripts/epic/epic-audit-rationale.ts +8 -2
  141. package/scripts/epic/epic-audit.ts +52 -1
  142. package/scripts/epic/epic-bundle-cli.ts +1 -2
  143. package/scripts/epic/epic-bundle-evidence.ts +49 -26
  144. package/scripts/epic/epic-bundle-referenced.ts +14 -3
  145. package/scripts/epic/epic-bundle.ts +17 -9
  146. package/scripts/epic/epic-chains.ts +6 -2
  147. package/scripts/epic/epic-close-comment.ts +1 -2
  148. package/scripts/followup/git-followup-workflow.ts +19 -1
  149. package/scripts/gate/check-inputs.ts +129 -0
  150. package/scripts/gate/check-reuse.ts +162 -0
  151. package/scripts/gate/core-budget.ts +93 -44
  152. package/scripts/gate/core-wait.ts +130 -0
  153. package/scripts/gate/free-cores.ts +49 -0
  154. package/scripts/gate/gate-ledger.ts +17 -3
  155. package/scripts/gate/gate-plan.ts +23 -1
  156. package/scripts/gate/gate-report.ts +15 -2
  157. package/scripts/gate/gate-run-steps.ts +150 -0
  158. package/scripts/gate/held-mark.ts +55 -0
  159. package/scripts/gate/machine-capacity.ts +70 -11
  160. package/scripts/gate/pre-commit-type-check.ts +14 -1
  161. package/scripts/gate/verification-gate.ts +65 -127
  162. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  163. package/scripts/gh/git-gh-issue-write.ts +24 -2
  164. package/scripts/gh/git-pr.ts +19 -3
  165. package/scripts/git/git-common-directory.ts +3 -3
  166. package/scripts/git/git-diff-reads.ts +16 -9
  167. package/scripts/git/git-execa-error.ts +1 -2
  168. package/scripts/git/main-merge-regenerate.ts +78 -0
  169. package/scripts/git/main-merge.ts +14 -5
  170. package/scripts/hooks/batch-guard.ts +5 -8
  171. package/scripts/hooks/format-edited-file.ts +10 -4
  172. package/scripts/hooks/pre-commit-lint.ts +91 -0
  173. package/scripts/hooks/pre-push-unit.ts +15 -4
  174. package/scripts/hooks/pretool-guard.ts +48 -79
  175. package/scripts/hooks/reserved-run.ts +1 -0
  176. package/scripts/hooks/run-hook.sh +10 -12
  177. package/scripts/hooks/step-zero-notice.ts +3 -2
  178. package/scripts/hooks/stop-actions.ts +204 -0
  179. package/scripts/hooks/stop-guard.ts +62 -21
  180. package/scripts/init/hook-command-rewrite.ts +6 -2
  181. package/scripts/init/hook-launch.ts +12 -7
  182. package/scripts/init/init-logic-deploy-vps.ts +4 -2
  183. package/scripts/issue/behavior-change-lint.ts +12 -6
  184. package/scripts/issue/expected-effect-lint.ts +59 -0
  185. package/scripts/issue/issue-cite-cli.ts +35 -15
  186. package/scripts/issue/issue-classification.ts +1 -1
  187. package/scripts/issue/issue-lint-cli.ts +5 -3
  188. package/scripts/issue/issue-scout-cli.ts +22 -16
  189. package/scripts/issue/markdown-section.ts +19 -5
  190. package/scripts/josh/command-usage.ts +110 -0
  191. package/scripts/josh/file-map-stamp.ts +1 -3
  192. package/scripts/josh/hook-decision.ts +17 -2
  193. package/scripts/josh/josh-command-map.ts +2 -0
  194. package/scripts/josh/josh-command-types.ts +55 -9
  195. package/scripts/josh/josh-commands-ai.ts +13 -0
  196. package/scripts/josh/josh-commands-development.ts +1 -1
  197. package/scripts/josh/josh-commands-hooks.ts +6 -0
  198. package/scripts/josh/josh-commands-lane.ts +10 -1
  199. package/scripts/josh/josh-commands-workflow.ts +5 -0
  200. package/scripts/josh/josh-logic.ts +6 -15
  201. package/scripts/josh/josh-usage-automation.ts +121 -0
  202. package/scripts/josh/josh-usage-backlog.ts +180 -0
  203. package/scripts/josh/josh-usage-developer.ts +230 -0
  204. package/scripts/josh/josh-usage-hooks.ts +38 -0
  205. package/scripts/josh/josh-usage-issue.ts +205 -0
  206. package/scripts/josh/josh-usage-review.ts +101 -0
  207. package/scripts/josh/josh-usage-run.ts +252 -0
  208. package/scripts/josh/josh-usage-session.ts +238 -0
  209. package/scripts/josh/josh-usage-workflow.ts +212 -0
  210. package/scripts/josh/josh.ts +14 -5
  211. package/scripts/josh/package-directory.ts +20 -0
  212. package/scripts/lane/lane-await.ts +3 -2
  213. package/scripts/lane/lane-capacity.ts +8 -2
  214. package/scripts/lane/lane-check-stats.ts +56 -0
  215. package/scripts/lane/lane-child-invocation.ts +49 -5
  216. package/scripts/lane/lane-dispatch.ts +3 -3
  217. package/scripts/lane/lane-guard-policy.ts +5 -5
  218. package/scripts/lane/lane-handoff.ts +8 -1
  219. package/scripts/lane/lane-install.ts +12 -4
  220. package/scripts/lane/lane-ledger.ts +52 -1
  221. package/scripts/lane/lane-limit-cli.ts +8 -9
  222. package/scripts/lane/lane-load.ts +9 -2
  223. package/scripts/lane/lane-measure-cli.ts +4 -0
  224. package/scripts/lane/lane-parked.ts +18 -0
  225. package/scripts/lane/lane-preserve.ts +29 -1
  226. package/scripts/lane/lane-process-load.ts +125 -0
  227. package/scripts/lane/lane-process-stats.ts +93 -0
  228. package/scripts/lane/lane-registry.ts +17 -10
  229. package/scripts/lane/lane-restart.ts +5 -26
  230. package/scripts/lane/lane-session-action.ts +146 -0
  231. package/scripts/lane/lane-session-broker.ts +68 -125
  232. package/scripts/lane/lane-spent.ts +14 -3
  233. package/scripts/lane/lane-stats.ts +1 -0
  234. package/scripts/lane/lane-withdraw-cli.ts +71 -0
  235. package/scripts/lane/lane-withdraw.ts +182 -0
  236. package/scripts/lint/format.ts +4 -4
  237. package/scripts/lint/lint-parallel.ts +10 -3
  238. package/scripts/lint/lint-related-scope.ts +4 -1
  239. package/scripts/managed-marker/managed-marker-logic.ts +1 -2
  240. package/scripts/notify/telegram-test.ts +10 -5
  241. package/scripts/overrides/lockfile-overrides.ts +1 -3
  242. package/scripts/propagate/propagate.ts +1 -2
  243. package/scripts/release/release-progress.ts +3 -2
  244. package/scripts/report/report-format-reference.ts +2 -0
  245. package/scripts/report/report-lint.ts +7 -4
  246. package/scripts/review/acceptance-map-check.ts +61 -0
  247. package/scripts/review/acceptance-map.ts +205 -0
  248. package/scripts/review/review-brief-cli.ts +1 -1
  249. package/scripts/review/review-record-cli.ts +6 -2
  250. package/scripts/rules/agent-model.ts +59 -0
  251. package/scripts/rules/delivered-rules.ts +11 -2
  252. package/scripts/rules/destructive-command.ts +6 -9
  253. package/scripts/rules/direct-filing.ts +3 -7
  254. package/scripts/rules/early-heartbeat.ts +7 -18
  255. package/scripts/rules/file-body.ts +6 -13
  256. package/scripts/rules/filing-cap.ts +4 -6
  257. package/scripts/rules/firing-point.ts +6 -5
  258. package/scripts/rules/git-force.ts +5 -9
  259. package/scripts/rules/guard-remedy.ts +117 -0
  260. package/scripts/rules/implementation-cut.ts +37 -35
  261. package/scripts/rules/index-guard.ts +4 -7
  262. package/scripts/rules/issue-comments.ts +67 -16
  263. package/scripts/rules/josh-git-bare.ts +3 -6
  264. package/scripts/rules/lane-background.ts +10 -21
  265. package/scripts/rules/lane-carry-conflict.ts +7 -17
  266. package/scripts/rules/lane-interactive-ask.ts +6 -11
  267. package/scripts/rules/lane-park.ts +5 -13
  268. package/scripts/rules/lane-split-park.ts +2 -7
  269. package/scripts/rules/lane-switch-main.ts +9 -12
  270. package/scripts/rules/last-prompt.ts +50 -5
  271. package/scripts/rules/oracle-consulted.ts +4 -5
  272. package/scripts/rules/oracle-list-cli.ts +7 -0
  273. package/scripts/rules/piped-verification.ts +84 -32
  274. package/scripts/rules/poll-loop.ts +7 -17
  275. package/scripts/rules/pre-gate-cut.ts +7 -21
  276. package/scripts/rules/prior-comment-read.ts +7 -5
  277. package/scripts/rules/protected-files.ts +5 -9
  278. package/scripts/rules/raw-field-body.ts +3 -7
  279. package/scripts/rules/rule-body-guard.ts +4 -11
  280. package/scripts/rules/rule-list.ts +4 -1
  281. package/scripts/rules/rule-registry-rows.ts +10 -0
  282. package/scripts/rules/rule-registry.ts +7 -4
  283. package/scripts/rules/rule-rewrite.ts +114 -0
  284. package/scripts/rules/rule-value.ts +7 -1
  285. package/scripts/rules/run-tail-rule.ts +6 -22
  286. package/scripts/rules/shell-body-trigger.ts +4 -6
  287. package/scripts/rules/shell-segments.ts +22 -0
  288. package/scripts/rules/stop-decision.ts +77 -0
  289. package/scripts/rules/stop-rules.ts +164 -77
  290. package/scripts/rules/tail-commands.ts +15 -1
  291. package/scripts/rules/test-declared-commit.ts +4 -10
  292. package/scripts/rules/third-party-write.ts +5 -10
  293. package/scripts/rules/worktree-guard.ts +5 -12
  294. package/scripts/run/board/run-board-cli.ts +2 -0
  295. package/scripts/run/board/run-board-closed.ts +8 -2
  296. package/scripts/run/board/run-board-github.ts +19 -3
  297. package/scripts/run/board/run-board-header.ts +4 -1
  298. package/scripts/run/board/run-board-idle.ts +31 -0
  299. package/scripts/run/board/run-board-labels.ts +2 -0
  300. package/scripts/run/board/run-board-layout.ts +1 -1
  301. package/scripts/run/board/run-board-read.ts +25 -12
  302. package/scripts/run/board/run-board-reason.ts +17 -4
  303. package/scripts/run/board/run-board-render-legend.ts +1 -0
  304. package/scripts/run/board/run-board-render-slot.ts +1 -1
  305. package/scripts/run/board/run-board-render-waits.ts +41 -0
  306. package/scripts/run/board/run-board-render.ts +6 -1
  307. package/scripts/run/board/run-board-row-yield.ts +1 -0
  308. package/scripts/run/board/run-board-state.ts +9 -0
  309. package/scripts/run/board/run-board-status.ts +89 -23
  310. package/scripts/run/board/run-board-thanks.ts +7 -2
  311. package/scripts/run/board/run-board-tick.ts +37 -8
  312. package/scripts/run/carry/run-carry-args.ts +1 -3
  313. package/scripts/run/carry/run-carry-conversation.ts +7 -2
  314. package/scripts/run/cut/run-cut-cli.ts +18 -22
  315. package/scripts/run/cut/run-cut-decline.ts +72 -0
  316. package/scripts/run/cut/run-cut-ready.ts +74 -0
  317. package/scripts/run/cut/run-cut-report.ts +29 -0
  318. package/scripts/run/cut/run-cut.ts +10 -1
  319. package/scripts/run/detached-launch.ts +23 -4
  320. package/scripts/run/entry/run-entry-cli.ts +8 -1
  321. package/scripts/run/event/run-event-render.ts +1 -0
  322. package/scripts/run/event/run-event-scope.ts +19 -1
  323. package/scripts/run/event/run-event-stream-emit.ts +13 -6
  324. package/scripts/run/event/run-event-stream.ts +4 -0
  325. package/scripts/run/hold/run-hold-cli.ts +14 -3
  326. package/scripts/run/hold/run-hold-session.ts +46 -0
  327. package/scripts/run/hold/run-hold.ts +13 -4
  328. package/scripts/run/launch-priority.ts +32 -0
  329. package/scripts/run/merge/run-merge-cli.ts +16 -42
  330. package/scripts/run/merge/run-merge-ship.ts +59 -1
  331. package/scripts/run/merge/run-merge-signals.ts +99 -0
  332. package/scripts/run/merge/run-merge-steps.ts +11 -1
  333. package/scripts/run/merge/run-merge.ts +22 -9
  334. package/scripts/run/run-ending.ts +31 -3
  335. package/scripts/run/run-liveness.ts +1 -1
  336. package/scripts/run/run-parent-cut-hook.ts +5 -10
  337. package/scripts/run/run-poll-hook.ts +157 -0
  338. package/scripts/run/run-preflight.ts +1 -2
  339. package/scripts/run/run-step-cli.ts +22 -15
  340. package/scripts/run/run-step-section.ts +85 -0
  341. package/scripts/run/run-stop-notify.ts +1 -2
  342. package/scripts/run/run-watcher-guard.ts +6 -3
  343. package/scripts/run/ship/run-ship-cli.ts +5 -12
  344. package/scripts/run/ship/run-ship-detach.ts +9 -0
  345. package/scripts/run/ship/run-ship-next.ts +10 -2
  346. package/scripts/run/ship/run-ship-overlap.ts +195 -0
  347. package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
  348. package/scripts/run/ship/run-ship-preflight.ts +28 -2
  349. package/scripts/run/ship/run-ship-return.ts +31 -7
  350. package/scripts/run/ship/run-ship-review-steps.ts +25 -14
  351. package/scripts/run/ship/run-ship-review.ts +15 -2
  352. package/scripts/run/ship/run-ship-steps.ts +1 -1
  353. package/scripts/run/ship/run-ship-stop-context.ts +206 -0
  354. package/scripts/run/ship/run-ship-sync.ts +14 -6
  355. package/scripts/run/tidy/run-tidy-cli.ts +3 -3
  356. package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
  357. package/scripts/safe-chain/project-config.ts +7 -2
  358. package/scripts/sync/sync-hook-safety.ts +8 -2
  359. package/scripts/test/test-e2e-guard.ts +30 -5
  360. package/scripts/test/test-unit-guard.ts +16 -1
  361. package/scripts/test/unit-worker-share.ts +26 -3
  362. package/scripts/time-runtime/time-batch-guard.ts +3 -3
  363. package/scripts/time-runtime/time-hook-transcript.ts +3 -2
  364. package/scripts/time-runtime/time-shell.ts +39 -4
  365. package/scripts/time-runtime/time-spans.ts +1 -0
  366. package/scripts/time-runtime/time-tool-call.ts +7 -0
  367. package/scripts/version/effective-upstream.ts +3 -4
  368. package/scripts/version/kit-version-config.ts +8 -2
  369. package/scripts/version/latest-corepack.ts +1 -2
  370. package/scripts/version/release-hold.ts +11 -4
  371. package/scripts/version/version-commands.ts +12 -4
  372. package/templates/gitignore +6 -2
  373. package/dist/hooks/chunk-D5QBTOCZ.js +0 -68
  374. package/dist/hooks/chunk-UT7YC3N2.js +0 -56
@@ -36,9 +36,16 @@ const KIND = {
36
36
  DISPATCH: 'dispatch',
37
37
  STAGE: 'stage',
38
38
  STOP: 'stop',
39
+ OVERLAP: 'overlap',
39
40
  } as const
40
41
 
41
42
  const merge_schema = z.object({ kind: z.literal(KIND.MERGE), at: z.string(), issue: z.number() })
43
+ const check_schema = z.object({
44
+ label: z.string(),
45
+ elapsed_ms: z.number(),
46
+ is_passed: z.boolean(),
47
+ is_reused: z.boolean().optional(),
48
+ })
42
49
  const gate_schema = z.object({
43
50
  kind: z.literal(KIND.GATE),
44
51
  at: z.string(),
@@ -50,12 +57,21 @@ const gate_schema = z.object({
50
57
  // The whole cores busy outside this gate, beyond the machine's baseline, at its start or its end —
51
58
  // whichever was higher. Absent when the machine could not be read.
52
59
  external_cores: z.number().optional(),
60
+ // Each check's own duration and verdict, so `josh lane:stats` can name the check a slow gate waited
61
+ // on. A reused check started no process: its row says so, and its zero is no timing.
62
+ checks: z.array(check_schema).optional(),
53
63
  })
54
64
  // `available_mb`, `swapped_mb` and `lanes` are optional because each read can fail on its own — a
55
65
  // platform with no memory reading, a `git worktree` call that did not answer — and a sample is still
56
66
  // worth its load. `swapped_mb` is `machine_capacity`'s counter, every page swapped since boot.
57
67
  // Both memory fields were renamed with their meaning (joshuafolkken/kit#3593): an older row's
58
68
  // `free_mb` / `swap_mb` are stripped on read, so the two measurements never share a column.
69
+ const process_kind_schema = z.object({
70
+ kind: z.string(),
71
+ cpu_percent: z.number(),
72
+ rss_mb: z.number(),
73
+ count: z.number(),
74
+ })
59
75
  const load_schema = z.object({
60
76
  kind: z.literal(KIND.LOAD),
61
77
  at: z.string(),
@@ -63,6 +79,9 @@ const load_schema = z.object({
63
79
  available_mb: z.number().optional(),
64
80
  swapped_mb: z.number().optional(),
65
81
  lanes: z.number().optional(),
82
+ // The heaviest process kinds at the instant of the sample (`lane-process-load.ts`), so the load the
83
+ // core budget's ledger does not explain can be read back by what made it (joshuafolkken/kit#3874).
84
+ processes: z.array(process_kind_schema).optional(),
66
85
  })
67
86
  // A lane child started on `issue` — the instant its implementation is timed from.
68
87
  const dispatch_schema = z.object({
@@ -89,6 +108,18 @@ const stop_schema = z.object({
89
108
  issue: z.number(),
90
109
  reason: z.string(),
91
110
  })
111
+ // A `josh ship` on `issue` whose actual change overlapped the open pull requests of `lanes` on `paths`
112
+ // before its sync: it waited `waited_ms` for their ships to end, and `is_cleared` is false where the
113
+ // wait ran out first (joshuafolkken/kit#3901).
114
+ const overlap_schema = z.object({
115
+ kind: z.literal(KIND.OVERLAP),
116
+ at: z.string(),
117
+ issue: z.number(),
118
+ lanes: z.array(z.number()),
119
+ paths: z.array(z.string()),
120
+ waited_ms: z.number(),
121
+ is_cleared: z.boolean(),
122
+ })
92
123
  const entry_schema = z.discriminatedUnion('kind', [
93
124
  merge_schema,
94
125
  gate_schema,
@@ -96,6 +127,7 @@ const entry_schema = z.discriminatedUnion('kind', [
96
127
  dispatch_schema,
97
128
  stage_schema,
98
129
  stop_schema,
130
+ overlap_schema,
99
131
  ])
100
132
 
101
133
  type LedgerEntry = z.infer<typeof entry_schema>
@@ -105,10 +137,13 @@ type LoadEntry = z.infer<typeof load_schema>
105
137
  type DispatchEntry = z.infer<typeof dispatch_schema>
106
138
  type StageEntry = z.infer<typeof stage_schema>
107
139
  type StopEntry = z.infer<typeof stop_schema>
140
+ type OverlapEntry = z.infer<typeof overlap_schema>
108
141
  // What a finished gate reports; the ledger stamps the kind and the time.
109
142
  type GateRecord = Omit<GateEntry, 'kind' | 'at'>
143
+ type GateCheckRecord = z.infer<typeof check_schema>
110
144
  type StageRecord = Omit<StageEntry, 'kind' | 'at'>
111
145
  type StopRecord = Omit<StopEntry, 'kind' | 'at'>
146
+ type OverlapRecord = Omit<OverlapEntry, 'kind' | 'at'>
112
147
 
113
148
  function target_of(repository: string): string {
114
149
  return stamp_file.stamp_path(LEDGER_PREFIX, repository, LEDGER_SUFFIX)
@@ -185,6 +220,10 @@ async function record_stop(stop: StopRecord, path?: string): Promise<void> {
185
220
  await record({ kind: KIND.STOP, at: now_iso(), ...stop }, path)
186
221
  }
187
222
 
223
+ async function record_overlap(overlap: OverlapRecord, path?: string): Promise<void> {
224
+ await record({ kind: KIND.OVERLAP, at: now_iso(), ...overlap }, path)
225
+ }
226
+
188
227
  function parse_line(line: string): LedgerEntry | undefined {
189
228
  return entry_schema.safeParse(json_value.parse_or_undefined(line)).data
190
229
  }
@@ -215,11 +254,23 @@ const lane_ledger = {
215
254
  record_dispatch,
216
255
  record_gate,
217
256
  record_merge,
257
+ record_overlap,
218
258
  record_stage,
219
259
  record_stop,
220
260
  target,
221
261
  target_of,
222
262
  }
223
263
 
224
- export type { DispatchEntry, GateEntry, LedgerEntry, LoadEntry, MergeEntry, StageEntry, StopEntry }
264
+ export type {
265
+ DispatchEntry,
266
+ GateCheckRecord,
267
+ GateEntry,
268
+ LedgerEntry,
269
+ LoadEntry,
270
+ MergeEntry,
271
+ OverlapEntry,
272
+ OverlapRecord,
273
+ StageEntry,
274
+ StopEntry,
275
+ }
225
276
  export { lane_ledger }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
- import { backlog_ready } from '#scripts/backlog/backlog-ready'
3
+ import { backlog_ready, type LaneUse } from '#scripts/backlog/backlog-ready'
4
4
  import { cli_flags } from '#scripts/lib/cli-flags'
5
5
  import { lane_capacity, type LimitChoice } from './lane-capacity'
6
6
  import { lane_limit_override } from './lane-limit-override'
@@ -37,13 +37,13 @@ interface LimitPorts {
37
37
  read_override: () => Promise<number | undefined>
38
38
  write_override: (limit: number | undefined) => Promise<boolean>
39
39
  emit_raise: (text: string) => Promise<void>
40
- live_lane_count: () => Promise<number>
40
+ lane_use: () => Promise<LaneUse>
41
41
  }
42
42
 
43
43
  interface LimitStatus {
44
44
  limit: number
45
45
  override: number | undefined
46
- in_use: number
46
+ use: LaneUse
47
47
  }
48
48
 
49
49
  const SHOW: LimitRequest = { kind: 'show' }
@@ -55,7 +55,7 @@ const DEFAULT_PORTS: LimitPorts = {
55
55
  read_override: lane_limit_override.read_override,
56
56
  write_override: lane_limit_override.write_override,
57
57
  emit_raise: lane_limit_override.emit_raise,
58
- live_lane_count: backlog_ready.live_lane_count,
58
+ lane_use: backlog_ready.lane_use,
59
59
  }
60
60
 
61
61
  function refuse(message: string): number {
@@ -98,9 +98,10 @@ function limit_text(limit: number): string {
98
98
 
99
99
  function status_line(status: LimitStatus): string {
100
100
  const source = status.override === undefined ? lane_capacity.LANE_LIMIT_KEY : OVERRIDE_SOURCE
101
- const free = lane_capacity.free_lanes(status.limit, status.in_use)
101
+ const { occupied, seated } = status.use
102
+ const free = lane_capacity.free_lanes(status.limit, occupied, seated)
102
103
 
103
- return `lane limit ${limit_text(status.limit)} (${source}) · in use ${String(status.in_use)} · free ${String(free)}`
104
+ return `lane limit ${limit_text(status.limit)} (${source}) · in use ${String(occupied)} · free ${String(free)}`
104
105
  }
105
106
 
106
107
  async function show(ports: LimitPorts): Promise<number> {
@@ -110,9 +111,7 @@ async function show(ports: LimitPorts): Promise<number> {
110
111
 
111
112
  const override = await ports.read_override()
112
113
 
113
- console.info(
114
- status_line({ limit: choice.limit, override, in_use: await ports.live_lane_count() }),
115
- )
114
+ console.info(status_line({ limit: choice.limit, override, use: await ports.lane_use() }))
116
115
 
117
116
  return SUCCESS_EXIT_CODE
118
117
  }
@@ -2,10 +2,12 @@ import { loadavg } from 'node:os'
2
2
  import { machine_capacity } from '#scripts/gate/machine-capacity'
3
3
  import { lane_await } from './lane-await'
4
4
  import type { LoadEntry } from './lane-ledger'
5
+ import { lane_process_load } from './lane-process-load'
5
6
  import { lane_registry, type LaneInfo } from './lane-registry'
6
7
 
7
8
  // One machine-load sample for the lane-limit measurement: the one-minute load
8
- // average, available memory, the swap counter and how many lanes have a child working, read at one
9
+ // average, available memory, the swap counter, how many lanes have a child working and the heaviest
10
+ // process kinds (`lane-process-load.ts`), read at one
9
11
  // instant so `josh lane:stats` can set the load beside the lane count that produced it.
10
12
  //
11
13
  // **The memory is `machine_capacity`'s reading, the one the gate admits against.** `os.freemem()` reads
@@ -38,7 +40,11 @@ function rounded(megabytes: number | undefined): number | undefined {
38
40
  }
39
41
 
40
42
  async function sample(at: string): Promise<LoadEntry> {
41
- const [memory, lanes] = await Promise.all([machine_capacity.read_memory(), read_lanes()])
43
+ const [memory, lanes, processes] = await Promise.all([
44
+ machine_capacity.read_memory(),
45
+ read_lanes(),
46
+ lane_process_load.read(),
47
+ ])
42
48
  const [load = 0] = loadavg()
43
49
 
44
50
  return {
@@ -48,6 +54,7 @@ async function sample(at: string): Promise<LoadEntry> {
48
54
  available_mb: rounded(memory.available_mb),
49
55
  swapped_mb: rounded(memory.swapped_mb),
50
56
  lanes,
57
+ processes,
51
58
  }
52
59
  }
53
60
 
@@ -3,7 +3,9 @@ import { setInterval as every } from 'node:timers/promises'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { cli_flags } from '#scripts/lib/cli-flags'
5
5
  import { lane_capacity } from './lane-capacity'
6
+ import { lane_check_stats } from './lane-check-stats'
6
7
  import { lane_ledger } from './lane-ledger'
8
+ import { lane_process_stats } from './lane-process-stats'
7
9
  import { lane_sampler, type MeasureContext } from './lane-sampler'
8
10
  import { lane_stage_stats } from './lane-stage-stats'
9
11
  import { lane_stats } from './lane-stats'
@@ -119,6 +121,8 @@ async function stats_command(
119
121
  `${lane_stats.header()}\n${row}`,
120
122
  lane_stage_stats.table(entries, window),
121
123
  lane_stop_stats.table(entries, window),
124
+ lane_process_stats.table(entries, window),
125
+ lane_check_stats.table(entries, window),
122
126
  ].join('\n\n'),
123
127
  )
124
128
 
@@ -0,0 +1,18 @@
1
+ import { has_label_name, NEEDS_DECISION_LABEL } from '#scripts/issue/issue-labels'
2
+ import { issue_state_cli } from '#scripts/issue/issue-state-cli'
3
+
4
+ // Whether an issue is parked for a person — the `needs-decision` label — read live, so a session a
5
+ // lane's broker would start for it is refused once the run has parked it (joshuafolkken/kit#3878).
6
+ //
7
+ // **An issue whose state cannot be read is not parked**: the read gates every brokered review, so
8
+ // reading an outage as a park would fail every review in flight for as long as GitHub is away.
9
+
10
+ async function is_parked(issue: string): Promise<boolean> {
11
+ const read = await issue_state_cli.read_issue(issue)
12
+
13
+ return read.kind === 'state' && has_label_name(read.state.labels, NEEDS_DECISION_LABEL)
14
+ }
15
+
16
+ const lane_parked = { is_parked }
17
+
18
+ export { lane_parked }
@@ -1,5 +1,6 @@
1
1
  import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
2
2
  import { git_stash } from '#scripts/git/stash/git-stash'
3
+ import { error_text } from '#scripts/lib/error-message'
3
4
 
4
5
  // Keeping a lane's uncommitted work before something discards or restarts it — `run:merge` closing a
5
6
  // lane, `lane:restart` handing it to a fresh session. The stash stack outlives the tree, so the push is
@@ -30,6 +31,32 @@ async function announce(issue: string, message: string, cause: string): Promise<
30
31
  await git_gh_issue_write.issue_try_comment(issue, preserved_comment(message, cause))
31
32
  }
32
33
 
34
+ // The tree's work kept before a stopped child's lane is reused or closed: `saved` with the stash, or
35
+ // none for a clean tree, and `unsaved` with the reason when the tree could not be read or pushed — the
36
+ // caller then leaves the tree as it is rather than handing it on or deleting it.
37
+ type KeptWork =
38
+ | { kind: 'saved'; stash: string | undefined }
39
+ | { kind: 'unsaved'; directory: string; reason: string }
40
+
41
+ async function stash_of(
42
+ issue: string,
43
+ directory: string,
44
+ occasion: string,
45
+ ): Promise<string | undefined> {
46
+ if (!(await git_stash.has_changes(directory))) return undefined
47
+
48
+ return await push_work(issue, directory, occasion)
49
+ }
50
+
51
+ /** Stash the tree's uncommitted work, if any, answering `unsaved` rather than throwing. */
52
+ async function keep_work(issue: string, directory: string, occasion: string): Promise<KeptWork> {
53
+ try {
54
+ return { kind: 'saved', stash: await stash_of(issue, directory, occasion) }
55
+ } catch (error) {
56
+ return { kind: 'unsaved', directory, reason: error_text.message_of(error) }
57
+ }
58
+ }
59
+
33
60
  /** `push_work`, then `announce` — for a caller with no reason to separate the two. */
34
61
  async function stash_work(
35
62
  issue: string,
@@ -44,6 +71,7 @@ async function stash_work(
44
71
  return message
45
72
  }
46
73
 
47
- const lane_preserve = { announce, push_work, stash_work }
74
+ const lane_preserve = { announce, keep_work, stash_work }
48
75
 
76
+ export type { KeptWork }
49
77
  export { lane_preserve }
@@ -0,0 +1,125 @@
1
+ import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
2
+ import { execa } from 'execa'
3
+
4
+ // What the machine's load is made of at one instant: every process grouped by the kind of tool it
5
+ // runs, the heaviest kinds kept with their CPU and resident size, for a `load` sample to carry.
6
+ //
7
+ // **Why a load sample needs it.** The gate's static checks and unit suite are on the core budget's
8
+ // ledger, so the load the ledger does not explain comes from somewhere else — review subagents, the
9
+ // sessions themselves, a hook's unit run, an IDE — and a load average alone cannot say which.
10
+ // joshuafolkken/kit#3874 measured load 93 with 1.6 lanes working and nothing on record to explain it.
11
+ //
12
+ // **The kind is read from the command line, not the executable name.** Every tool here is a `node`
13
+ // process, so the name says nothing; the first pattern a command line matches names its kind, in the
14
+ // order below, so `pnpm exec vitest` is `vitest` rather than `josh`. The IDE comes first: its language
15
+ // servers run out of a `typescript` or `eslint` directory, and read as `tsc` or `eslint` they would
16
+ // pass the IDE's load off as the gate's own checks — and a server the IDE starts on a plain `node`
17
+ // runtime is known only by its `.vscode/extensions/` path. `claude` is the executable or its package,
18
+ // never a `.claude/` path a session's Bash-tool shell or hook script runs through. `pnpm` is the
19
+ // command, never the `.pnpm/` store a resolved binary's path runs through.
20
+ //
21
+ // **`%cpu` is the share of one core**, so 300 is three cores busy; it is `ps`'s decaying average,
22
+ // which is what a one-minute sample beside the one-minute load average wants.
23
+
24
+ const PS_ARGUMENTS = ['-A', '-o', 'rss=,%cpu=,args=']
25
+ // `ps` prints `%cpu` with the locale's decimal separator, and a `0,7` would parse as NaN.
26
+ const PS_ENVIRONMENT = { LC_ALL: 'C' }
27
+ const FIELD_SEPARATOR = /\s+/u
28
+ const KB_PER_MB = 1024
29
+ // Enough to name what dominates and the runner-up behind it, short enough for one ledger line a minute.
30
+ const TOP_KINDS = 5
31
+ const OTHER_KIND = 'other'
32
+
33
+ const KIND_PATTERNS: ReadonlyArray<readonly [string, RegExp]> = [
34
+ ['vscode', /Visual Studio Code|Code Helper|\.vscode\/extensions\//u],
35
+ ['vitest', /vitest/u],
36
+ ['eslint', /eslint/u],
37
+ ['tsc', /\btsc\b|svelte-check|typescript/u],
38
+ ['cspell', /cspell/u],
39
+ ['prettier', /prettier/u],
40
+ ['playwright', /playwright/u],
41
+ ['chrome', /Google Chrome/u],
42
+ ['chromium', /chromium|headless_shell/iu],
43
+ ['claude', /(?:^|\/)claude(?:\s|$)|@anthropic-ai\/claude-code\//u],
44
+ ['josh', /josh\.ts|\btsx\b|(?<!\.)\bpnpm\b/u],
45
+ ['node', /\bnode\b/u],
46
+ ]
47
+
48
+ interface ProcessKind {
49
+ kind: string
50
+ cpu_percent: number
51
+ rss_mb: number
52
+ count: number
53
+ }
54
+
55
+ function kind_of(command_line: string): string {
56
+ return KIND_PATTERNS.find(([, pattern]) => pattern.test(command_line))?.[0] ?? OTHER_KIND
57
+ }
58
+
59
+ // One line of `ps`'s listing as its kind and readings; a header or a cut-short line reads as nothing.
60
+ function process_of(line: string): Omit<ProcessKind, 'count'> | undefined {
61
+ const [rss, cpu, ...command] = line.trim().split(FIELD_SEPARATOR)
62
+ const reading = { cpu_percent: Number(cpu), rss_mb: Number(rss) / KB_PER_MB }
63
+
64
+ if (command.length === 0 || Number.isNaN(reading.cpu_percent + reading.rss_mb)) return undefined
65
+
66
+ return { kind: kind_of(command.join(' ')), ...reading }
67
+ }
68
+
69
+ function add_line(totals: Map<string, ProcessKind>, line: string): void {
70
+ const reading = process_of(line)
71
+
72
+ if (reading === undefined) return
73
+
74
+ const { kind } = reading
75
+ const total = totals.get(kind) ?? { kind, cpu_percent: 0, rss_mb: 0, count: 0 }
76
+
77
+ total.cpu_percent += reading.cpu_percent
78
+ total.rss_mb += reading.rss_mb
79
+ total.count += 1
80
+ totals.set(kind, total)
81
+ }
82
+
83
+ function rounded(total: ProcessKind): ProcessKind {
84
+ return { ...total, cpu_percent: Math.round(total.cpu_percent), rss_mb: Math.round(total.rss_mb) }
85
+ }
86
+
87
+ // The heaviest kinds first: by CPU, the load average's own measure, then by memory for idle kinds.
88
+ function by_weight(left: ProcessKind, right: ProcessKind): number {
89
+ return right.cpu_percent - left.cpu_percent || right.rss_mb - left.rss_mb
90
+ }
91
+
92
+ function summarize(listing: string): Array<ProcessKind> {
93
+ const totals = new Map<string, ProcessKind>()
94
+
95
+ for (const line of listing.split('\n')) add_line(totals, line)
96
+
97
+ return [...totals.values()]
98
+ .map((total) => rounded(total))
99
+ .toSorted(by_weight)
100
+ .slice(0, TOP_KINDS)
101
+ }
102
+
103
+ // `undefined` where `ps` did not answer, so the sample records no breakdown rather than an empty one.
104
+ async function read(): Promise<Array<ProcessKind> | undefined> {
105
+ try {
106
+ const { stdout } = await execa('ps', PS_ARGUMENTS, {
107
+ env: PS_ENVIRONMENT,
108
+ timeout: PROBE_TIMEOUT_MS,
109
+ })
110
+
111
+ return summarize(stdout)
112
+ } catch {
113
+ return undefined
114
+ }
115
+ }
116
+
117
+ const lane_process_load = {
118
+ TOP_KINDS,
119
+ kind_of,
120
+ read,
121
+ summarize,
122
+ }
123
+
124
+ export type { ProcessKind }
125
+ export { lane_process_load }
@@ -0,0 +1,93 @@
1
+ import type { LedgerEntry, LoadEntry } from './lane-ledger'
2
+ import type { ProcessKind } from './lane-process-load'
3
+ import { lane_stats, type StatsWindow } from './lane-stats'
4
+
5
+ // What the period's machine load was made of: the process kinds the load samples carried
6
+ // (`lane-process-load.ts`), one row per kind, printed by `josh lane:stats` under the stop table so the
7
+ // load the core budget's ledger does not explain is read back by what made it (joshuafolkken/kit#3874).
8
+ //
9
+ // **A kind is averaged over the samples it appears in**, never over every sample. A sample keeps only
10
+ // its heaviest kinds, so a kind absent from one was below them rather than idle, and a zero there would
11
+ // understate it; `samples` says how often it was among the heaviest, which is the weight to read it by.
12
+ //
13
+ // **CPU is in cores** — `ps`'s share of one core over a hundred — so the column reads against the load
14
+ // average and the machine's core count without a conversion.
15
+
16
+ const COLUMNS = [
17
+ 'process',
18
+ 'cpu mean (cores)',
19
+ 'cpu peak (cores)',
20
+ 'rss peak (GB)',
21
+ 'samples',
22
+ ] as const
23
+ const PERCENT_PER_CORE = 100
24
+ const MB_PER_GB = 1024
25
+ const DECIMALS = 1
26
+
27
+ interface KindRow {
28
+ kind: string
29
+ readings: Array<ProcessKind>
30
+ }
31
+
32
+ function is_load(entry: LedgerEntry): entry is LoadEntry {
33
+ return entry.kind === 'load'
34
+ }
35
+
36
+ function group(samples: ReadonlyArray<LoadEntry>): Array<KindRow> {
37
+ const rows = new Map<string, KindRow>()
38
+ const readings = samples.flatMap((sample) => sample.processes ?? [])
39
+
40
+ for (const reading of readings) {
41
+ const row = rows.get(reading.kind) ?? { kind: reading.kind, readings: [] }
42
+
43
+ row.readings.push(reading)
44
+ rows.set(reading.kind, row)
45
+ }
46
+
47
+ return [...rows.values()]
48
+ }
49
+
50
+ function cores(readings: ReadonlyArray<ProcessKind>): Array<number> {
51
+ return readings.map((reading) => reading.cpu_percent / PERCENT_PER_CORE)
52
+ }
53
+
54
+ // A row has one reading at least, so its mean always exists.
55
+ function mean_cores(row: KindRow): number {
56
+ return lane_stats.mean(cores(row.readings)) ?? 0
57
+ }
58
+
59
+ function cell(value: number | undefined): string {
60
+ return value === undefined ? lane_stats.MISSING : value.toFixed(DECIMALS)
61
+ }
62
+
63
+ function kind_cells(row: KindRow): Array<string> {
64
+ const rss_peak = lane_stats.peak(row.readings.map((reading) => reading.rss_mb))
65
+
66
+ return [
67
+ row.kind,
68
+ cell(mean_cores(row)),
69
+ cell(lane_stats.peak(cores(row.readings))),
70
+ cell(rss_peak === undefined ? undefined : rss_peak / MB_PER_GB),
71
+ String(row.readings.length),
72
+ ]
73
+ }
74
+
75
+ function by_mean_cpu(left: KindRow, right: KindRow): number {
76
+ return mean_cores(right) - mean_cores(left)
77
+ }
78
+
79
+ // The period's table: `entries` is the whole ledger, filtered here to `window`. A period whose samples
80
+ // carried no breakdown prints the header alone.
81
+ function table(entries: ReadonlyArray<LedgerEntry>, window: StatsWindow): string {
82
+ const rows = group(lane_stats.in_window(entries, window).filter(is_load)).toSorted(by_mean_cpu)
83
+
84
+ return [
85
+ lane_stats.to_row(COLUMNS),
86
+ lane_stats.to_row(COLUMNS.map(() => '---')),
87
+ ...rows.map((row) => lane_stats.to_row(kind_cells(row))),
88
+ ].join('\n')
89
+ }
90
+
91
+ const lane_process_stats = { table }
92
+
93
+ export { lane_process_stats }
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, statSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { ENV_FILE_NAME } from '#ports'
4
4
  import type { AgentProfile } from '#scripts/agent/agent-role-profile'
@@ -38,6 +38,9 @@ interface LaneInfo {
38
38
  // instead of waiting for the pool to drain before it can be cut.
39
39
  output: string | undefined
40
40
  profile?: AgentProfile | undefined
41
+ // When the lane last took a child: `lane:open` writes its `.env`, and every dispatch records its
42
+ // unit there (`lane-output.ts`). `undefined` where the `.env` cannot be read.
43
+ dispatched_ms?: number | undefined
41
44
  is_stranded: boolean
42
45
  }
43
46
 
@@ -106,6 +109,10 @@ function read_environment(directory: string): string | undefined {
106
109
  return file_reader.read_if_readable(path.join(directory, ENV_FILE_NAME))
107
110
  }
108
111
 
112
+ function dispatched_at(directory: string): number | undefined {
113
+ return statSync(path.join(directory, ENV_FILE_NAME), { throwIfNoEntry: false })?.mtimeMs
114
+ }
115
+
109
116
  // A lane is identified by its branch rather than by where it sits, so nothing here compares two
110
117
  // spellings of one path — git prints the resolved one, and a lane root reached through a symlink
111
118
  // would otherwise read as no lane at all.
@@ -151,6 +158,7 @@ function build_lane(
151
158
  preview_port: held.preview_port,
152
159
  output,
153
160
  profile,
161
+ dispatched_ms: is_stranded ? undefined : dispatched_at(directory),
154
162
  is_stranded,
155
163
  }
156
164
  }
@@ -175,9 +183,14 @@ function parse_block(block: string, root: string): LaneInfo | undefined {
175
183
  const branch = line_value(lines, BRANCH_PREFIX)
176
184
  const issue = branch_issue(branch)
177
185
 
178
- if (directory === undefined || branch === undefined || issue === undefined) return undefined
179
-
180
- if (!is_lane_directory(root, issue, directory)) return undefined
186
+ if (
187
+ directory === undefined ||
188
+ branch === undefined ||
189
+ issue === undefined ||
190
+ !is_lane_directory(root, issue, directory)
191
+ ) {
192
+ return undefined
193
+ }
181
194
 
182
195
  return build_lane(issue, branch, directory, is_gone(directory))
183
196
  }
@@ -236,11 +249,6 @@ async function has_lanes_in_flight(): Promise<boolean> {
236
249
  return lanes.some((lane) => !lane.is_stranded)
237
250
  }
238
251
 
239
- /** The lanes in use: the open work trees an interruption has not stranded. */
240
- function live_count(lanes: ReadonlyArray<LaneInfo>): number {
241
- return lanes.filter((lane) => !lane.is_stranded).length
242
- }
243
-
244
252
  /** The seats live lanes hold — what a new lane's seat has to avoid. */
245
253
  function used_seats(lanes: ReadonlyArray<LaneInfo>): Array<number> {
246
254
  return lanes.map((lane) => lane.seat).filter((seat): seat is number => seat !== undefined)
@@ -272,7 +280,6 @@ const lane_registry = {
272
280
  find_open_lane,
273
281
  has_lanes_in_flight,
274
282
  list_lanes,
275
- live_count,
276
283
  main_repository_root,
277
284
  parse_block,
278
285
  read_environment,
@@ -1,14 +1,12 @@
1
- import { git_stash } from '#scripts/git/stash/git-stash'
2
1
  import { issue_cite } from '#scripts/issue/issue-cite'
3
2
  import { IN_PROGRESS_LABEL } from '#scripts/issue/issue-labels'
4
3
  import { issue_number_shape } from '#scripts/issue/issue-number-shape'
5
4
  import { session_cite } from '#scripts/issue/session-cite'
6
- import { error_text } from '#scripts/lib/error-message'
7
5
  import { run_cut } from '#scripts/run/cut/run-cut'
8
6
  import { run_event_stream } from '#scripts/run/event/run-event-stream'
9
7
  import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
10
8
  import { lane_dispatch, type DispatchOutcome } from './lane-dispatch'
11
- import { lane_preserve } from './lane-preserve'
9
+ import { lane_preserve, type KeptWork } from './lane-preserve'
12
10
  import { lane_reap } from './lane-reap'
13
11
  import { lane_registry, type LaneInfo } from './lane-registry'
14
12
 
@@ -38,11 +36,7 @@ const NO_LANE_KIND = 'no-lane'
38
36
  const NOT_RUNNING_KIND = 'not-running'
39
37
  const UNCLAIMED_KIND = 'unclaimed'
40
38
 
41
- interface Unsaved {
42
- kind: 'unsaved'
43
- directory: string
44
- reason: string
45
- }
39
+ type Unsaved = Extract<KeptWork, { kind: 'unsaved' }>
46
40
 
47
41
  interface Restarted {
48
42
  kind: 'restarted'
@@ -55,7 +49,6 @@ type Refused =
55
49
  | { kind: typeof NOT_RUNNING_KIND }
56
50
  | { kind: typeof UNCLAIMED_KIND; reason: string }
57
51
  type RestartOutcome = Restarted | Unsaved | Refused
58
- type Saved = Unsaved | { kind: 'saved'; stash: string | undefined }
59
52
 
60
53
  // A cut the stopped child carried would hand its successor the old session's hand-off at
61
54
  // `run:cut --resume`; the fresh session has to start from the Issue instead.
@@ -65,22 +58,6 @@ function end_carried_cut(lane: LaneInfo): void {
65
58
  if (carried !== undefined) run_cut.end_cut(carried.target)
66
59
  }
67
60
 
68
- async function stash_of(lane: LaneInfo): Promise<string | undefined> {
69
- if (!(await git_stash.has_changes(lane.directory))) return undefined
70
-
71
- return await lane_preserve.push_work(lane.issue, lane.directory, RESTART_OCCASION)
72
- }
73
-
74
- // A tree that could not be read or pushed is left as it is: re-dispatching into it would hand the fresh
75
- // session the stopped one's half-done work as though it were its own.
76
- async function save_work(lane: LaneInfo): Promise<Saved> {
77
- try {
78
- return { kind: 'saved', stash: await stash_of(lane) }
79
- } catch (error) {
80
- return { kind: 'unsaved', directory: lane.directory, reason: error_text.message_of(error) }
81
- }
82
- }
83
-
84
61
  async function record_restart(issue: string): Promise<void> {
85
62
  await run_event_stream_emit.emit(
86
63
  run_event_stream.EVENT_KIND.RESTART,
@@ -88,9 +65,11 @@ async function record_restart(issue: string): Promise<void> {
88
65
  )
89
66
  }
90
67
 
68
+ // A tree that could not be read or pushed is left as it is: re-dispatching into it would hand the fresh
69
+ // session the stopped one's half-done work as though it were its own.
91
70
  async function relaunch(lane: LaneInfo): Promise<RestartOutcome> {
92
71
  end_carried_cut(lane)
93
- const saved = await save_work(lane)
72
+ const saved = await lane_preserve.keep_work(lane.issue, lane.directory, RESTART_OCCASION)
94
73
 
95
74
  if (saved.kind === 'unsaved') return saved
96
75
  const dispatch = await lane_dispatch.dispatch_child(lane.issue, 'restart')