@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
@@ -0,0 +1,146 @@
1
+ import { agent_argv } from '#scripts/agent/agent-argv'
2
+ import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
3
+ import { stamp_file } from '#scripts/josh/stamp-file'
4
+ import { error_text } from '#scripts/lib/error-message'
5
+ import { detached_launch, type AttachedLaunchResult } from '#scripts/run/detached-launch'
6
+ import { run_ship_review } from '#scripts/run/ship/run-ship-review'
7
+ import { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
8
+ import { run_ship_stop_context } from '#scripts/run/ship/run-ship-stop-context'
9
+ import { z } from 'zod'
10
+ import { lane_child_invocation, type ShipStop } from './lane-child-invocation'
11
+ import { lane_child_marker } from './lane-child-marker'
12
+ import { lane_parked } from './lane-parked'
13
+ import type { LaneInfo } from './lane-registry'
14
+ import { lane_relaunch } from './lane-relaunch'
15
+
16
+ // The actions a lane's session broker performs for the ship that asked (`lane-session-broker.ts`):
17
+ // starting the round-1 or round-2 reviewer, or relaunching the lane's child after a stopped ship.
18
+ // **A failure names its reason**: a bare `false` left every relaunch a stale supervisor refused
19
+ // unexplained in the ship log (joshuafolkken/kit#3974).
20
+
21
+ const BRIEF_PREFIX = 'josh-ship-review-brief-'
22
+ const FINDINGS_PREFIX = 'josh-ship-review-findings-'
23
+ const LOG_PREFIX = 'josh-ship-review-log-'
24
+ const ANTHROPIC_PROVIDER: AgentProvider = 'anthropic'
25
+ const stop_schema = z
26
+ .object({
27
+ stage: z.enum(run_ship_stage.STAGE),
28
+ title: z.string(),
29
+ flags: z.array(z.string()).readonly(),
30
+ is_review: z.boolean(),
31
+ conflicts: z.array(z.string()).readonly().default([]),
32
+ context: run_ship_stop_context.stop_context_schema.exactOptional(),
33
+ })
34
+ .strict()
35
+ const action_schema = z.discriminatedUnion('kind', [
36
+ z.object({ kind: z.literal('review'), round: z.enum(['1', '2']) }).strict(),
37
+ z.object({ kind: z.literal('relaunch'), stop: stop_schema }).strict(),
38
+ ])
39
+ type ReviewRound = Extract<z.infer<typeof action_schema>, { kind: 'review' }>['round']
40
+ type BrokerAction = { kind: 'review'; round: ReviewRound } | { kind: 'relaunch'; stop: ShipStop }
41
+ type BrokerAnswer = { is_success: true } | { is_success: false; reason: string }
42
+
43
+ const SUCCESS: BrokerAnswer = { is_success: true }
44
+
45
+ function failure(reason: string): BrokerAnswer {
46
+ return { is_success: false, reason }
47
+ }
48
+
49
+ function review_paths(directory: string): { brief: string; findings: string; log: string } {
50
+ return {
51
+ brief: stamp_file.stamp_path(BRIEF_PREFIX, directory, '.md'),
52
+ findings: stamp_file.stamp_path(FINDINGS_PREFIX, directory, '.txt'),
53
+ log: stamp_file.stamp_path(LOG_PREFIX, directory, '.log'),
54
+ }
55
+ }
56
+
57
+ // The reviewer runs on the lane's own provider, so a Claude lane's server never starts a Codex reviewer
58
+ // and an OpenAI lane's never starts a Claude one.
59
+ function provider_of(lane: LaneInfo): AgentProvider {
60
+ return lane.profile?.provider ?? ANTHROPIC_PROVIDER
61
+ }
62
+
63
+ // **The reviewer carries the lane's mark explicitly**: the supervisor serving a Claude lane is no lane child,
64
+ // so a reviewer inheriting its environment would descend from the carry record's owner unmarked and be
65
+ // read as the `backlogrun` parent by every hook. **Its prompt ends with the review invocation**, so the
66
+ // lane's liveness read and the park's reap find it as they find the child (joshuafolkken/kit#3878).
67
+
68
+ function review_prompt(lane: LaneInfo, round: ReviewRound): string {
69
+ const target = review_paths(lane.directory)
70
+ const prompt_of =
71
+ round === '1' ? run_ship_review.reviewer_prompt : run_ship_review.verification_prompt
72
+ const tail = lane_child_invocation.review_invocation(lane.issue)
73
+
74
+ return `${prompt_of(target.brief, target.findings, lane.issue)} This is ${tail}`
75
+ }
76
+
77
+ function reviewer_answer(result: AttachedLaunchResult): BrokerAnswer {
78
+ if (result.kind === 'failed') return failure(`the reviewer could not start: ${result.note}`)
79
+
80
+ return result.exit_code === 0
81
+ ? SUCCESS
82
+ : failure(`the reviewer exited with code ${String(result.exit_code)}`)
83
+ }
84
+
85
+ async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<BrokerAnswer> {
86
+ const target = review_paths(lane.directory)
87
+ const built = agent_argv.review_in(review_prompt(lane, round), Number(round), lane.directory)
88
+ if (built.kind === 'rejected') return failure(`the reviewer profile was rejected: ${built.note}`)
89
+
90
+ if (built.profile.provider !== provider_of(lane)) {
91
+ return failure(`the reviewer resolved to ${built.profile.provider}, not the lane's provider`)
92
+ }
93
+
94
+ console.error(agent_role_profile.describe_review(Number(round), built.profile))
95
+ const result = await detached_launch.launch_attached(
96
+ {
97
+ argv: built.argv,
98
+ cwd: lane.directory,
99
+ log_path: target.log,
100
+ profile: built.profile,
101
+ env: lane_child_marker.env_for(lane.issue),
102
+ },
103
+ console.error,
104
+ )
105
+
106
+ return reviewer_answer(result)
107
+ }
108
+
109
+ // An OpenAI lane's successor is its supervisor's to start, never a relaunch's.
110
+ function relaunch(lane: LaneInfo, stop: ShipStop): BrokerAnswer {
111
+ if (lane_relaunch.is_openai_lane(lane)) {
112
+ return failure('an OpenAI lane is relaunched by its own supervisor')
113
+ }
114
+
115
+ const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
116
+ const phase = agent_role_profile.IMPLEMENTATION_PHASE
117
+ const result = lane_relaunch.relaunch(lane, invocation, phase, console.error)
118
+
119
+ return result.kind === 'launched'
120
+ ? SUCCESS
121
+ : failure(`the relaunch did not start: ${result.note}`)
122
+ }
123
+
124
+ // **A parked issue gets no session**: a request can be read long after it was written, so the park is
125
+ // read when the action runs, never when it was asked for (joshuafolkken/kit#3878).
126
+ async function perform(lane: LaneInfo, action: BrokerAction): Promise<BrokerAnswer> {
127
+ if (await lane_parked.is_parked(lane.issue)) return failure('the issue is parked')
128
+
129
+ if (action.kind === 'relaunch') return relaunch(lane, action.stop)
130
+
131
+ return await launch_review(lane, action.round)
132
+ }
133
+
134
+ // What the server answers: a thrown action is a failure naming its error, never a dead server.
135
+ async function answer(lane: LaneInfo, action: BrokerAction): Promise<BrokerAnswer> {
136
+ try {
137
+ return await perform(lane, action)
138
+ } catch (error) {
139
+ return failure(`the broker failed: ${error_text.message_of(error)}`)
140
+ }
141
+ }
142
+
143
+ const lane_session_action = { SUCCESS, action_schema, answer, failure, review_paths }
144
+
145
+ export type { BrokerAction, BrokerAnswer, ReviewRound }
146
+ export { lane_session_action }
@@ -1,22 +1,14 @@
1
1
  import { randomUUID } from 'node:crypto'
2
- import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
2
+ import { mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { setTimeout as sleep } from 'node:timers/promises'
5
- import { agent_argv } from '#scripts/agent/agent-argv'
6
- import { agent_role_profile, type AgentProvider } from '#scripts/agent/agent-role-profile'
7
5
  import { git_common_directory } from '#scripts/git/git-common-directory'
8
6
  import { stamp_file } from '#scripts/josh/stamp-file'
9
- import { error_text } from '#scripts/lib/error-message'
10
7
  import { json_value } from '#scripts/lib/json-value'
11
- import { detached_launch } from '#scripts/run/detached-launch'
12
- import { run_ship_review } from '#scripts/run/ship/run-ship-review'
13
- import { run_ship_stage } from '#scripts/run/ship/run-ship-stage'
14
8
  import { z } from 'zod'
15
9
  import { lane_broker_server, type BrokerServer } from './lane-broker-server'
16
- import { lane_child_invocation, type ShipStop } from './lane-child-invocation'
17
- import { lane_child_marker } from './lane-child-marker'
18
10
  import type { LaneInfo } from './lane-registry'
19
- import { lane_relaunch } from './lane-relaunch'
11
+ import { lane_session_action, type BrokerAction, type BrokerAnswer } from './lane-session-action'
20
12
 
21
13
  // The sessions a lane child's detached `josh ship` cannot start itself, started for it by a process
22
14
  // that can: the round-1 and round-2 reviewer, and the fresh child a stopped ship hands back to. An
@@ -24,13 +16,11 @@ import { lane_relaunch } from './lane-relaunch'
24
16
  // Claude lane is served by the `run:wake` supervisor (`claude-lane-broker.ts`), because Claude Code removes its
25
17
  // token from what its Bash tool starts, so a token-only credential never reaches the ship the child
26
18
  // launched (joshuafolkken/kit#3817). **The request names an action, never a command**: the server
27
- // rebuilds the argv from the registered lane, so a child can ask for nothing but these two sessions.
19
+ // rebuilds the argv from the registered lane, so a child can ask for nothing but these two sessions
20
+ // (`lane-session-action.ts`).
28
21
 
29
22
  const REQUEST_PREFIX = '.josh-review-request-'
30
23
  const RESPONSE_PREFIX = '.josh-review-response-'
31
- const BRIEF_PREFIX = 'josh-ship-review-brief-'
32
- const FINDINGS_PREFIX = 'josh-ship-review-findings-'
33
- const LOG_PREFIX = 'josh-ship-review-log-'
34
24
  const CACHE_DIRECTORY = ['node_modules', '.cache']
35
25
  const POLL_MS = 100
36
26
  // The heartbeat is kept for a ship branched before the server record, and read only where a
@@ -40,22 +30,8 @@ const LEGACY_HEARTBEAT_LIMIT_MS = 3000
40
30
  const MINUTE_MS = 60_000
41
31
  const REQUEST_LIMIT_MINUTES = 30
42
32
  const REQUEST_LIMIT_MS = REQUEST_LIMIT_MINUTES * MINUTE_MS
43
- const ANTHROPIC_PROVIDER: AgentProvider = 'anthropic'
44
- const stop_schema = z
45
- .object({
46
- stage: z.enum(run_ship_stage.STAGE),
47
- title: z.string(),
48
- flags: z.array(z.string()).readonly(),
49
- is_review: z.boolean(),
50
- conflicts: z.array(z.string()).readonly().default([]),
51
- })
52
- .strict()
53
- const action_schema = z.discriminatedUnion('kind', [
54
- z.object({ kind: z.literal('review'), round: z.enum(['1', '2']) }).strict(),
55
- z.object({ kind: z.literal('relaunch'), stop: stop_schema }).strict(),
56
- ])
57
33
  const request_schema = z
58
- .object({ session: z.uuid(), nonce: z.uuid(), action: action_schema })
34
+ .object({ session: z.uuid(), nonce: z.uuid(), action: lane_session_action.action_schema })
59
35
  .strict()
60
36
  const response_schema = z
61
37
  .object({
@@ -65,18 +41,13 @@ const response_schema = z
65
41
  heartbeat_at: z.number().int().positive(),
66
42
  })
67
43
  .strict()
44
+ // **A failure's reason sits beside the response, never in it**: a ship branched before the reason parses
45
+ // the response strictly, so a new field would read as no server (joshuafolkken/kit#3974).
46
+ const reason_schema = z.object({ nonce: z.string(), reason: z.string() }).strict()
68
47
  type BrokerRequest = z.infer<typeof request_schema>
69
48
  type BrokerResponse = z.infer<typeof response_schema>
70
- type ReviewRound = Extract<z.infer<typeof action_schema>, { kind: 'review' }>['round']
71
- type BrokerAction = { kind: 'review'; round: ReviewRound } | { kind: 'relaunch'; stop: ShipStop }
72
-
73
- function review_paths(directory: string): { brief: string; findings: string; log: string } {
74
- return {
75
- brief: stamp_file.stamp_path(BRIEF_PREFIX, directory, '.md'),
76
- findings: stamp_file.stamp_path(FINDINGS_PREFIX, directory, '.txt'),
77
- log: stamp_file.stamp_path(LOG_PREFIX, directory, '.log'),
78
- }
79
- }
49
+
50
+ const { failure } = lane_session_action
80
51
 
81
52
  function request_path(directory: string, issue: string): string {
82
53
  return path.join(directory, ...CACHE_DIRECTORY, `${REQUEST_PREFIX}${issue}.json`)
@@ -89,6 +60,10 @@ function response_path(directory: string, issue: string): string | undefined {
89
60
  return path.join(path.dirname(common), ...CACHE_DIRECTORY, `${RESPONSE_PREFIX}${issue}.json`)
90
61
  }
91
62
 
63
+ function reason_path(response: string): string {
64
+ return `${response}.reason`
65
+ }
66
+
92
67
  function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
93
68
  try {
94
69
  return json_value.parse_with(readFileSync(target, 'utf8'), schema)
@@ -97,90 +72,25 @@ function read_record<T>(target: string, schema: z.ZodType<T>): T | undefined {
97
72
  }
98
73
  }
99
74
 
100
- // The reviewer runs on the lane's own provider, so a Claude lane's server never starts a Codex reviewer
101
- // and an OpenAI lane's never starts a Claude one.
102
- function provider_of(lane: LaneInfo): AgentProvider {
103
- return lane.profile?.provider ?? ANTHROPIC_PROVIDER
104
- }
105
-
106
- // **The reviewer carries the lane's mark explicitly**: the supervisor serving a Claude lane is no lane child,
107
- // so a reviewer inheriting its environment would descend from the carry record's owner unmarked and be
108
- // read as the `backlogrun` parent by every hook.
109
-
110
- async function launch_review(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
111
- const target = review_paths(lane.directory)
112
- const prompt_of =
113
- round === '1' ? run_ship_review.reviewer_prompt : run_ship_review.verification_prompt
114
- const built = agent_argv.resolve_in(
115
- prompt_of(target.brief, target.findings),
116
- agent_role_profile.REVIEWER,
117
- lane.directory,
118
- )
119
- if (built.kind === 'rejected' || built.profile.provider !== provider_of(lane)) return false
120
-
121
- const result = await detached_launch.launch_attached(
122
- {
123
- argv: built.argv,
124
- cwd: lane.directory,
125
- log_path: target.log,
126
- profile: built.profile,
127
- env: lane_child_marker.env_for(lane.issue),
128
- },
129
- console.error,
130
- )
131
-
132
- return result.kind === 'completed' && result.exit_code === 0
133
- }
134
-
135
- // An OpenAI lane's successor is its supervisor's to start, never a relaunch's.
136
- function relaunch(lane: LaneInfo, stop: ShipStop): boolean {
137
- if (lane_relaunch.is_openai_lane(lane)) return false
138
-
139
- const invocation = lane_child_invocation.ship_stop_invocation(lane.issue, stop)
140
- const phase = agent_role_profile.IMPLEMENTATION_PHASE
141
- const result = lane_relaunch.relaunch(lane, invocation, phase, console.error)
142
-
143
- return result.kind === 'launched'
144
- }
145
-
146
- async function perform(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
147
- if (action.kind === 'relaunch') return relaunch(lane, action.stop)
148
-
149
- return await launch_review(lane, action.round)
150
- }
151
-
152
- function write_response(target: string, body: BrokerResponse): void {
75
+ function write_record(target: string, body: BrokerResponse | z.infer<typeof reason_schema>): void {
153
76
  const temporary = `${target}.${String(process.pid)}.tmp`
154
77
 
155
78
  writeFileSync(temporary, JSON.stringify(body))
156
79
  renameSync(temporary, target)
157
80
  }
158
81
 
159
- async function launched(lane: LaneInfo, action: BrokerAction): Promise<boolean> {
160
- try {
161
- return await perform(lane, action)
162
- } catch (error) {
163
- error_text.trace_swallowed('lane_session_broker.launched', error)
164
-
165
- return false
166
- }
167
- }
168
-
169
82
  async function process_request(
170
83
  lane: LaneInfo,
171
84
  response: string,
172
85
  received: BrokerRequest,
173
86
  state: { current: BrokerResponse },
174
87
  ): Promise<void> {
175
- const is_success = await launched(lane, received.action)
88
+ const answer = await lane_session_action.answer(lane, received.action)
89
+ const { session, nonce } = received
176
90
 
177
- state.current = {
178
- session: received.session,
179
- nonce: received.nonce,
180
- is_success,
181
- heartbeat_at: Date.now(),
182
- }
183
- write_response(response, state.current)
91
+ if (!answer.is_success) write_record(reason_path(response), { nonce, reason: answer.reason })
92
+ state.current = { session, nonce, is_success: answer.is_success, heartbeat_at: Date.now() }
93
+ write_record(response, state.current)
184
94
  }
185
95
 
186
96
  async function serve(
@@ -223,7 +133,7 @@ function prepare(lane: LaneInfo): { response: string; current: BrokerResponse }
223
133
  heartbeat_at: Date.now(),
224
134
  }
225
135
 
226
- write_response(response, current)
136
+ write_record(response, current)
227
137
 
228
138
  return { response, current }
229
139
  }
@@ -231,7 +141,7 @@ function prepare(lane: LaneInfo): { response: string; current: BrokerResponse }
231
141
  function start_heartbeat(state: { current: BrokerResponse }, response: string): NodeJS.Timeout {
232
142
  const heartbeat = setInterval(() => {
233
143
  state.current = { ...state.current, heartbeat_at: Date.now() }
234
- write_response(response, state.current)
144
+ write_record(response, state.current)
235
145
  }, LEGACY_HEARTBEAT_MS)
236
146
 
237
147
  heartbeat.unref()
@@ -251,6 +161,7 @@ async function stop_server(
251
161
  await serving
252
162
  clearInterval(heartbeat)
253
163
  unlinkSync(response)
164
+ rmSync(reason_path(response), { force: true })
254
165
  lane_broker_server.remove(response)
255
166
  }
256
167
 
@@ -293,31 +204,47 @@ function session_response(target: string, session: string): BrokerResponse | und
293
204
  return response?.session === session ? response : undefined
294
205
  }
295
206
 
207
+ // A failure from a server that predates the reason leaves none.
208
+ function answer_of(target: string, response: BrokerResponse): BrokerAnswer {
209
+ if (response.is_success) return lane_session_action.SUCCESS
210
+
211
+ const record = read_record(reason_path(target), reason_schema)
212
+ const reason = record?.nonce === response.nonce ? record.reason : 'it gave no reason'
213
+
214
+ return failure(`the broker answered a failure: ${reason}`)
215
+ }
216
+
217
+ // The server's answer to this request, or `undefined` while it is still being served.
296
218
  function response_verdict(
297
219
  target: string,
298
220
  session: string,
299
221
  nonce: string,
300
- ): 'wait' | 'success' | 'failure' {
222
+ ): BrokerAnswer | undefined {
301
223
  const response = session_response(target, session)
302
224
 
303
- if (!is_live_response(target, response, lane_broker_server.is_running)) return 'failure'
304
- if (response.nonce !== nonce) return 'wait'
225
+ if (!is_live_response(target, response, lane_broker_server.is_running)) {
226
+ return failure('the broker stopped serving before it answered')
227
+ }
305
228
 
306
- return response.is_success ? 'success' : 'failure'
229
+ return response.nonce === nonce ? answer_of(target, response) : undefined
307
230
  }
308
231
 
309
- async function wait_response(target: string, session: string, nonce: string): Promise<boolean> {
232
+ async function wait_response(
233
+ target: string,
234
+ session: string,
235
+ nonce: string,
236
+ ): Promise<BrokerAnswer> {
310
237
  const deadline = Date.now() + REQUEST_LIMIT_MS
311
238
 
312
239
  while (Date.now() < deadline) {
313
240
  const verdict = response_verdict(target, session, nonce)
314
- if (verdict !== 'wait') return verdict === 'success'
241
+ if (verdict !== undefined) return verdict
315
242
 
316
243
  // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
317
244
  await sleep(POLL_MS)
318
245
  }
319
246
 
320
- return false
247
+ return failure(`the broker gave no answer within ${String(REQUEST_LIMIT_MINUTES)} minutes`)
321
248
  }
322
249
 
323
250
  function live_response(target: string): BrokerResponse | undefined {
@@ -336,18 +263,33 @@ function is_serving(directory: string, issue: string): boolean {
336
263
  return target !== undefined && live_response(target) !== undefined
337
264
  }
338
265
 
339
- async function request(directory: string, issue: string, action: BrokerAction): Promise<boolean> {
266
+ // **A request nobody waits for any more is taken back**: a server busy with an earlier request reads the
267
+ // file only once it is free, and a relaunch the ship had already given up on then started a session in a
268
+ // lane the run had parked in the meantime (joshuafolkken/kit#3878). A newer request is left alone.
269
+ function withdraw(pending: string, nonce: string): void {
270
+ if (read_record(pending, request_schema)?.nonce === nonce) stamp_file.remove_stamp(pending)
271
+ }
272
+
273
+ async function request(
274
+ directory: string,
275
+ issue: string,
276
+ action: BrokerAction,
277
+ ): Promise<BrokerAnswer> {
340
278
  const target = response_path(directory, issue)
341
279
  const response = target === undefined ? undefined : live_response(target)
342
280
 
343
- if (target === undefined || response === undefined) return false
281
+ if (target === undefined || response === undefined) return failure('no broker serves this lane')
344
282
 
345
283
  const nonce = randomUUID()
346
284
  const body = { session: response.session, nonce, action }
285
+ const pending = request_path(directory, issue)
347
286
 
348
- stamp_file.replace_stamp(request_path(directory, issue), body)
287
+ stamp_file.replace_stamp(pending, body)
288
+ const answer = await wait_response(target, response.session, nonce)
349
289
 
350
- return await wait_response(target, response.session, nonce)
290
+ withdraw(pending, nonce)
291
+
292
+ return answer
351
293
  }
352
294
 
353
295
  const lane_session_broker = {
@@ -355,9 +297,10 @@ const lane_session_broker = {
355
297
  request,
356
298
  request_path,
357
299
  response_path,
358
- review_paths,
300
+ review_paths: lane_session_action.review_paths,
359
301
  with_server,
360
302
  }
361
303
 
362
- export type { BrokerAction, ReviewRound }
363
304
  export { lane_session_broker }
305
+
306
+ export { type ReviewRound, type BrokerAction, type BrokerAnswer } from './lane-session-action'
@@ -178,10 +178,13 @@ function issues_traced(traced: ReadonlyArray<TracedLane>, trace: ProcessTrace):
178
178
  return traced.filter(([, answer]) => answer === trace).map(([issue]) => issue)
179
179
  }
180
180
 
181
+ function traced_of(lanes: ReadonlyArray<LaneInfo>, probes: SpentProbes): Array<TracedLane> {
182
+ return lanes.map((lane): TracedLane => [lane.issue, trace_of(lane, probes)])
183
+ }
184
+
181
185
  /** The lanes in use — what a run being replaced must not be stopped over. */
182
186
  async function lanes_in_use(probes: SpentProbes = LIVE_PROBES): Promise<LaneUse> {
183
- const lanes = await lane_registry.list_lanes()
184
- const traced = lanes.map((lane): TracedLane => [lane.issue, trace_of(lane, probes)])
187
+ const traced = traced_of(await lane_registry.list_lanes(), probes)
185
188
 
186
189
  return {
187
190
  working: issues_traced(traced, PROCESS_ALIVE),
@@ -189,7 +192,15 @@ async function lanes_in_use(probes: SpentProbes = LIVE_PROBES): Promise<LaneUse>
189
192
  }
190
193
  }
191
194
 
192
- const lane_spent = { kept_reason, lanes_in_use, sort_lanes }
195
+ /** The lanes no child is working in, read as such — never one whose probe did not answer. */
196
+ function idle_lanes(
197
+ lanes: ReadonlyArray<LaneInfo>,
198
+ probes: SpentProbes = LIVE_PROBES,
199
+ ): Array<string> {
200
+ return issues_traced(traced_of(lanes, probes), PROCESS_NONE)
201
+ }
202
+
203
+ const lane_spent = { idle_lanes, kept_reason, lanes_in_use, sort_lanes }
193
204
 
194
205
  export type { KeptLane, LaneUse, SpentProbes, SpentSort }
195
206
  export { lane_spent }
@@ -258,6 +258,7 @@ const lane_stats = {
258
258
  MISSING,
259
259
  header,
260
260
  in_window,
261
+ mean,
261
262
  median,
262
263
  peak,
263
264
  row,
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env tsx
2
+ import { fileURLToPath } from 'node:url'
3
+ import { issue_number_shape } from '#scripts/issue/issue-number-shape'
4
+ import { cli_flags } from '#scripts/lib/cli-flags'
5
+ import { lane_withdraw, type WithdrawOutcome } from './lane-withdraw'
6
+
7
+ // `josh lane:withdraw <issue> --reason <text>` — every explanation on standard error, and a withdrawal
8
+ // that did not end with the Issue closed exits non-zero, so a caller never takes a half-done one as done.
9
+
10
+ const ARGV_OFFSET = 2
11
+ const SUCCESS_EXIT_CODE = 0
12
+ const FAILURE_EXIT_CODE = 1
13
+ const USAGE = 'Usage: josh lane:withdraw <issue-number> --reason <text>'
14
+ const OPTIONS = { reason: { type: 'string' } } as const
15
+
16
+ interface Request {
17
+ issue: string
18
+ reason: string
19
+ }
20
+
21
+ function issue_of(positionals: ReadonlyArray<string>): string | undefined {
22
+ const [issue, ...rest] = positionals
23
+
24
+ return rest.length === 0 && issue_number_shape.is_issue_number(issue ?? '') ? issue : undefined
25
+ }
26
+
27
+ // A reason of only whitespace is no reason: the Issue would close with an empty `Why:` line.
28
+ function reason_of(value: unknown): string | undefined {
29
+ const reason = cli_flags.string_of(value)?.trim()
30
+
31
+ return reason === '' ? undefined : reason
32
+ }
33
+
34
+ function parse_request(argv: ReadonlyArray<string>): Request | undefined {
35
+ const parsed = cli_flags.arguments_of(argv, OPTIONS)
36
+ if (parsed === undefined) return undefined
37
+ const issue = issue_of(parsed.positionals)
38
+ const reason = reason_of(parsed.values.reason)
39
+
40
+ return issue === undefined || reason === undefined ? undefined : { issue, reason }
41
+ }
42
+
43
+ function exit_code_of(outcome: WithdrawOutcome): number {
44
+ return outcome.kind === 'withdrawn' && outcome.is_closed ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
45
+ }
46
+
47
+ async function run(argv: ReadonlyArray<string>): Promise<number> {
48
+ const request = parse_request(argv)
49
+
50
+ if (request === undefined) {
51
+ console.error(USAGE)
52
+
53
+ return FAILURE_EXIT_CODE
54
+ }
55
+
56
+ const outcome = await lane_withdraw.withdraw(request.issue, request.reason)
57
+
58
+ console.error(lane_withdraw.describe(outcome, request.issue))
59
+
60
+ return exit_code_of(outcome)
61
+ }
62
+
63
+ async function main(argv: ReadonlyArray<string>): Promise<void> {
64
+ process.exitCode = await run(argv)
65
+ }
66
+
67
+ const lane_withdraw_cli = { run }
68
+
69
+ if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
70
+
71
+ export { lane_withdraw_cli }