@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,60 @@
1
+ // The text an agent launch may carry: a model id, an effort and a prompt, refused when empty, over
2
+ // length or carrying a control character, so nothing a value holds can inject a further argument.
3
+
4
+ const FIRST_PRINTABLE_CODE = 0x20
5
+ const DELETE_CODE = 0x7f
6
+ const FIRST_CODE_POINT = 0
7
+ const EMPTY_LENGTH = 0
8
+ const MAX_VALUE_LENGTH = 4096
9
+ const LINE_FEED = '\n'
10
+ const SPACE = ' '
11
+
12
+ function is_control_character(character: string): boolean {
13
+ const code = character.codePointAt(FIRST_CODE_POINT) ?? FIRST_PRINTABLE_CODE
14
+
15
+ return code < FIRST_PRINTABLE_CODE || code === DELETE_CODE
16
+ }
17
+
18
+ function is_line_feed(character: string): boolean {
19
+ return character === LINE_FEED
20
+ }
21
+
22
+ function is_never_allowed(): boolean {
23
+ return false
24
+ }
25
+
26
+ function is_refused(character: string, is_allowed: (character: string) => boolean): boolean {
27
+ return is_control_character(character) && !is_allowed(character)
28
+ }
29
+
30
+ function is_safe_text(value: string, is_allowed: (character: string) => boolean): boolean {
31
+ if (value.length === EMPTY_LENGTH || value.length > MAX_VALUE_LENGTH) return false
32
+
33
+ for (const character of value) if (is_refused(character, is_allowed)) return false
34
+
35
+ return true
36
+ }
37
+
38
+ function is_safe_value(value: string): boolean {
39
+ return is_safe_text(value, is_never_allowed)
40
+ }
41
+
42
+ // **An agent's prompt is the one value a line break belongs in.** A ship-stop prompt carries the failed
43
+ // output's lines (joshuafolkken/kit#3964); every other control character is refused as in any value.
44
+ function is_safe_prompt(value: string): boolean {
45
+ return is_safe_text(value, is_line_feed)
46
+ }
47
+
48
+ function printable_character(character: string): string {
49
+ return is_refused(character, is_line_feed) ? SPACE : character
50
+ }
51
+
52
+ // Command output made fit for a prompt: every control character but a line break becomes a space, so
53
+ // the text passes `is_safe_prompt` whatever the command printed.
54
+ function printable(text: string): string {
55
+ return Array.from(text, printable_character).join('')
56
+ }
57
+
58
+ const agent_safe_text = { MAX_VALUE_LENGTH, is_safe_prompt, is_safe_value, printable }
59
+
60
+ export { agent_safe_text }
@@ -6,6 +6,7 @@ import {
6
6
  type AgentProfile,
7
7
  type AgentRole,
8
8
  } from './agent-role-profile'
9
+ import { lane_instructions } from './lane-instructions'
9
10
 
10
11
  const AGENT_COMMAND = 'claude'
11
12
  // **The permission mode is passed, not left to the checkout's `.claude/settings.json`.** Claude Code
@@ -63,6 +64,7 @@ const PROJECT_MCP_FILE = '.mcp.json'
63
64
  interface ClaudeArgv {
64
65
  command: string
65
66
  args: ReadonlyArray<string>
67
+ ends_with_prompt: true
66
68
  }
67
69
 
68
70
  type ClaudeArgvResult =
@@ -87,6 +89,14 @@ function tool_flags(cwd: string = process.cwd()): ReadonlyArray<string> {
87
89
  return [TOOLS_FLAG, LAUNCHED_TOOLS.join(','), ...mcp_flags]
88
90
  }
89
91
 
92
+ // **A lane child and a review session start from the lane instruction set** (`lane-instructions.ts`);
93
+ // the scheduler keeps `CLAUDE.md`, because it is the one that stops and asks a person.
94
+ function instruction_flags(profile: AgentProfile, cwd?: string): ReadonlyArray<string> {
95
+ if (profile.role === agent_role_profile.SCHEDULER) return []
96
+
97
+ return lane_instructions.flags(cwd)
98
+ }
99
+
90
100
  function build(
91
101
  invocation: string,
92
102
  profile: AgentProfile,
@@ -98,6 +108,7 @@ function build(
98
108
  args: [
99
109
  ...AGENT_FLAGS,
100
110
  ...tool_flags(cwd),
111
+ ...instruction_flags(profile, cwd),
101
112
  ...session_id_flags(session_id),
102
113
  MODEL_FLAG,
103
114
  profile.model,
@@ -105,6 +116,7 @@ function build(
105
116
  profile.effort,
106
117
  invocation,
107
118
  ],
119
+ ends_with_prompt: true,
108
120
  }
109
121
  }
110
122
 
@@ -121,6 +133,7 @@ function build_resume(
121
133
  args: [
122
134
  ...AGENT_FLAGS,
123
135
  ...tool_flags(cwd),
136
+ ...instruction_flags(profile, cwd),
124
137
  RESUME_FLAG,
125
138
  session_id,
126
139
  MODEL_FLAG,
@@ -129,6 +142,7 @@ function build_resume(
129
142
  profile.effort,
130
143
  invocation,
131
144
  ],
145
+ ends_with_prompt: true,
132
146
  }
133
147
  }
134
148
 
@@ -19,6 +19,7 @@ const LEDGER_DIRECTORY = path.dirname(OBSERVATION_LEDGER_DIRECTORY)
19
19
  interface CodexArgv {
20
20
  command: string
21
21
  args: ReadonlyArray<string>
22
+ ends_with_prompt: true
22
23
  }
23
24
 
24
25
  function effort_config(profile: AgentProfile): string {
@@ -47,8 +48,13 @@ function ledger_arguments(
47
48
  common_directory: string | undefined,
48
49
  profile: AgentProfile,
49
50
  ): ReadonlyArray<string> {
50
- if (common_directory === undefined || profile.role !== agent_role_profile.WORKER) return []
51
- if (path.basename(common_directory) !== GIT_DIRECTORY) return []
51
+ if (
52
+ common_directory === undefined ||
53
+ profile.role !== agent_role_profile.WORKER ||
54
+ path.basename(common_directory) !== GIT_DIRECTORY
55
+ ) {
56
+ return []
57
+ }
52
58
 
53
59
  const directory = path.join(path.dirname(common_directory), LEDGER_DIRECTORY)
54
60
 
@@ -86,6 +92,7 @@ function build(invocation: string, profile: AgentProfile, cwd?: string): CodexAr
86
92
  JSON_FLAG,
87
93
  invocation,
88
94
  ],
95
+ ends_with_prompt: true,
89
96
  }
90
97
  }
91
98
 
@@ -0,0 +1,144 @@
1
+ import path from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { cost_tokens } from '#scripts/cost-runtime/cost-tokens'
4
+ import { document_section } from '#scripts/document/document-section'
5
+ import { distributed_paths } from '#scripts/init/distributed-paths'
6
+ import { find_package_directory } from '#scripts/josh/package-directory'
7
+ import { stamp_file } from '#scripts/josh/stamp-file'
8
+
9
+ interface LaneSet {
10
+ text: string
11
+ rules_path: string
12
+ }
13
+
14
+ // **A headless lane child or review session starts from the lane instruction set, not `CLAUDE.md`.**
15
+ // `CLAUDE.md` carries rules only an interactive session can act on — consultation versus execution,
16
+ // explicit workflow invocation, stop notifications — and every request re-reads it. The lane document
17
+ // names the rules a lane does need by `CLAUDE.md` heading or bold label, and each `include` line is
18
+ // expanded from the launch checkout's own `CLAUDE.md` here, so the rule text keeps one source (#3929).
19
+ const LANE_DOCUMENT = 'prompts/collaboration-workflow/lane-instructions.md'
20
+ const RULES_FILE = 'CLAUDE.md'
21
+ // `AGENTS.md` only points at `CLAUDE.md`, and Claude Code loads it once `CLAUDE.md` is excluded.
22
+ const POINTER_FILE = 'AGENTS.md'
23
+ const INCLUDE_PATTERN = /^<!-- include: CLAUDE\.md → "(?<wanted>.+)" -->$/u
24
+ const PROMPT_FILE_FLAG = '--append-system-prompt-file'
25
+ const SETTINGS_FLAG = '--settings'
26
+ const PROMPT_FILE_PREFIX = 'josh-lane-instructions-'
27
+ const PROMPT_FILE_SUFFIX = '.md'
28
+ const PACKAGE_DIRECTORY = find_package_directory(path.dirname(fileURLToPath(import.meta.url)))
29
+
30
+ // An include that resolves to nothing is `undefined`, never an empty string, so a renamed heading
31
+ // fails the whole composition rather than silently dropping a rule.
32
+ function expand_line(line: string, rules: string): string | undefined {
33
+ const wanted = INCLUDE_PATTERN.exec(line)?.groups?.['wanted']
34
+
35
+ if (wanted === undefined) return line
36
+
37
+ return document_section.section(rules, wanted)?.text
38
+ }
39
+
40
+ function compose(lane_markdown: string, rules: string): string | undefined {
41
+ const expanded = lane_markdown.split('\n').map((line) => expand_line(line, rules))
42
+
43
+ if (expanded.includes(undefined)) return undefined
44
+
45
+ return expanded.join('\n')
46
+ }
47
+
48
+ // **Where the rules are: the checkout's `CLAUDE.md`, else the package copy it imports.** kit and a
49
+ // consumer holding a full copy keep them in `CLAUDE.md`; a consumer written by `josh init` imports
50
+ // them from the package, and its own `CLAUDE.md` — the bootstrap note and the project's additions —
51
+ // stays loaded, because only the file the set was composed from is excluded.
52
+ function rules_sources(cwd: string): ReadonlyArray<string> {
53
+ const imported = distributed_paths.CLAUDE_MD_IMPORT_LINE.replace(/^@/u, '')
54
+
55
+ return [path.join(cwd, RULES_FILE), path.join(cwd, imported)]
56
+ }
57
+
58
+ function composed_from(lane_markdown: string, rules_path: string): string | undefined {
59
+ const rules = document_section.read_optional(rules_path)
60
+
61
+ return rules === undefined ? undefined : compose(lane_markdown, rules)
62
+ }
63
+
64
+ function composed_set(cwd: string, package_directory: string): LaneSet | undefined {
65
+ const lane_markdown = document_section.read_optional(path.join(package_directory, LANE_DOCUMENT))
66
+
67
+ if (lane_markdown === undefined) return undefined
68
+
69
+ for (const rules_path of rules_sources(cwd)) {
70
+ const text = composed_from(lane_markdown, rules_path)
71
+
72
+ if (text !== undefined) return { text, rules_path }
73
+ }
74
+
75
+ return undefined
76
+ }
77
+
78
+ function composed_in(
79
+ cwd: string,
80
+ package_directory: string = PACKAGE_DIRECTORY,
81
+ ): string | undefined {
82
+ return composed_set(cwd, package_directory)?.text
83
+ }
84
+
85
+ // Named by its content and this account, so concurrent launches of the same text share one file and a
86
+ // changed `CLAUDE.md` never reuses a stale one. The name is predictable in a temp directory other
87
+ // accounts may write to, so an existing file is reused only when it is this account's own regular file
88
+ // holding exactly this text; anything else is replaced through `stamp_file`, which refuses another
89
+ // account's file and renames a new one into place whole. A file rather than the text itself, because
90
+ // the launcher refuses a line break in any argv element but the prompt.
91
+ function written(text: string): string | undefined {
92
+ const file_path = stamp_file.stamp_path(PROMPT_FILE_PREFIX, text, PROMPT_FILE_SUFFIX)
93
+
94
+ if (stamp_file.read_stamp_text(file_path) === text) return file_path
95
+
96
+ try {
97
+ return stamp_file.replace_text_stamp(file_path, text)
98
+ } catch {
99
+ return undefined
100
+ }
101
+ }
102
+
103
+ function excluded_settings(rules_path: string, cwd: string): string {
104
+ return JSON.stringify({ claudeMdExcludes: [rules_path, path.join(cwd, POINTER_FILE)] })
105
+ }
106
+
107
+ // No flags when the set cannot be composed or safely written: the child then loads `CLAUDE.md` as
108
+ // before, which costs tokens but never loses a rule.
109
+ function flags(cwd: string = process.cwd()): ReadonlyArray<string> {
110
+ const set = composed_set(cwd, PACKAGE_DIRECTORY)
111
+ const prompt_file = set === undefined ? undefined : written(set.text)
112
+
113
+ if (set === undefined || prompt_file === undefined) return []
114
+
115
+ return [PROMPT_FILE_FLAG, prompt_file, SETTINGS_FLAG, excluded_settings(set.rules_path, cwd)]
116
+ }
117
+
118
+ function tokens_of(text: string): string {
119
+ return cost_tokens.estimate(text).toLocaleString('en-US')
120
+ }
121
+
122
+ // `read:set lane-child`'s resident line: what each lane request carries in place of the rules file.
123
+ function rules_tokens(rules_path: string): string {
124
+ return tokens_of(document_section.read_optional(rules_path) ?? '')
125
+ }
126
+
127
+ function resident_line(cwd: string = process.cwd()): string {
128
+ const set = composed_set(cwd, PACKAGE_DIRECTORY)
129
+ const rules = rules_tokens(set?.rules_path ?? path.join(cwd, RULES_FILE))
130
+ const lane_tokens = set === undefined ? 'not composed' : `${tokens_of(set.text)} tok`
131
+
132
+ return ` resident: lane instructions ${lane_tokens} in place of ${RULES_FILE} ${rules} tok`
133
+ }
134
+
135
+ const lane_instructions = {
136
+ PROMPT_FILE_FLAG,
137
+ SETTINGS_FLAG,
138
+ compose,
139
+ composed_in,
140
+ flags,
141
+ resident_line,
142
+ }
143
+
144
+ export { lane_instructions }
@@ -157,8 +157,7 @@ function build_input(values: ParsedValues, now_ms: number): BudgetInput | undefi
157
157
 
158
158
  const started_at_ms = to_time_ms(cli_flags.string_of(values.started))
159
159
 
160
- if (started_at_ms === undefined) return undefined
161
- if (!are_values_readable(values)) return undefined
160
+ if (started_at_ms === undefined || !are_values_readable(values)) return undefined
162
161
 
163
162
  return input_of(values, { answer, now_ms, started_at_ms })
164
163
  }
@@ -42,7 +42,7 @@ const WATCH_VERDICT = 'watch'
42
42
  const STOP_VERDICT = 'stop'
43
43
  const TRIAGE_VERDICT = 'triage'
44
44
 
45
- // `run` — start what `backlog:next` offered. `watch` — sleep the polling interval and ask again,
45
+ // `run` — start what `backlog:next` offered. `watch` — sleep the re-ask floor and ask again,
46
46
  // except while something of the run's own is in flight, where the wake is the progress watcher's exit
47
47
  // and the interval is only a floor (`docs/maintainers/backlogrun-driver.md` → "Waiting while
48
48
  // something is in flight").
@@ -87,7 +87,7 @@ interface BudgetDecision {
87
87
  }
88
88
 
89
89
  const BLOCKED_REASON =
90
- 'Everything opted in is blocked or already running, so waiting can still change the answer. Poll again at the polling interval.'
90
+ 'Everything opted in is blocked or already running, so waiting can still change the answer. Ask again at the re-ask floor (60 s).'
91
91
 
92
92
  const TRIAGE_REASON =
93
93
  'An opted-in candidate carries neither `run:solo` nor `run:lane`, so nothing starts until it is judged. Read each issue named above, record its order and one of the two labels with a comment, then ask again.'
@@ -121,7 +121,7 @@ function max_reached_reason(merged: number, max_issues: number): string {
121
121
  }
122
122
 
123
123
  function draining_reason(running: number, reason: string): string {
124
- return `Start nothing more and poll until the running children merge (running: ${String(running)}). Then report and finish — ${reason}`
124
+ return `Start nothing more and wait on \`pnpm josh lane:await\` until the running children merge (running: ${String(running)}). Then report and finish — ${reason}`
125
125
  }
126
126
 
127
127
  function idle_expired_reason(answer: BacklogAnswer, idle_budget_ms: number): string {
@@ -140,7 +140,7 @@ function idle_watch_reason(answer: BacklogAnswer, left_ms: number): string {
140
140
  // **The watch runs down while they drain, and restarts at each one collected.** A collected child is
141
141
  // work, so it moves `active_at`; an ask that offered nothing never does.
142
142
  function running_watch_reason(answer: BacklogAnswer, running: number, left_ms: number): string {
143
- return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree until they merge and poll at the polling interval — the watch is running down meanwhile.`
143
+ return `${waiting_on(answer)} and the idle watch has about ${to_minutes(left_ms)} minutes left, with ${String(running)} still running. Keep the working tree and wait on \`pnpm josh lane:await\` until they merge — the watch is running down meanwhile.`
144
144
  }
145
145
 
146
146
  function watching_reason(input: BudgetInput, left_ms: number): string {
@@ -0,0 +1,26 @@
1
+ import { has_label_name, IN_PROGRESS_LABEL } from '#scripts/issue/issue-labels'
2
+ import { issue_state_cli } from '#scripts/issue/issue-state-cli'
3
+ import type { LaneInfo } from '#scripts/lane/lane-registry'
4
+
5
+ // The open lanes an earlier invocation dispatched that still claim their issue: `in-progress`, and not
6
+ // stopped for a person's review. Their unit may have died across the hand-off, so the drive collects
7
+ // them; a lane parked or held for review is a person's and is left alone.
8
+
9
+ async function is_claimed(issue: string): Promise<boolean> {
10
+ const read = await issue_state_cli.read_issue(issue)
11
+
12
+ if (read.kind !== 'state') return false
13
+
14
+ return has_label_name(read.state.labels, IN_PROGRESS_LABEL) && !read.state.is_human_review
15
+ }
16
+
17
+ async function carried(lanes: ReadonlyArray<LaneInfo>): Promise<ReadonlyArray<string>> {
18
+ const dispatched = lanes.filter((lane) => lane.output !== undefined).map((lane) => lane.issue)
19
+ const claims = await Promise.all(dispatched.map(async (issue) => await is_claimed(issue)))
20
+
21
+ return dispatched.filter((_issue, index) => claims[index] === true)
22
+ }
23
+
24
+ const backlog_drive_carried = { carried }
25
+
26
+ export { backlog_drive_carried }
@@ -6,7 +6,7 @@ import { issue_cite } from '#scripts/issue/issue-cite'
6
6
  import { hook_decision } from '#scripts/josh/hook-decision'
7
7
  import { josh_command } from '#scripts/josh/josh-run'
8
8
  import { lane_await, type AwaitState } from '#scripts/lane/lane-await'
9
- import { lane_registry } from '#scripts/lane/lane-registry'
9
+ import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
10
10
  import { lane_sampler } from '#scripts/lane/lane-sampler'
11
11
  import { error_text } from '#scripts/lib/error-message'
12
12
  import { json_value } from '#scripts/lib/json-value'
@@ -24,6 +24,7 @@ import {
24
24
  type OfferRead,
25
25
  } from './backlog-drive'
26
26
  import { backlog_drive_args, type DriveContext } from './backlog-drive-args'
27
+ import { backlog_drive_carried } from './backlog-drive-carried'
27
28
  import { backlog_drive_finish } from './backlog-drive-finish'
28
29
  import { backlog_drive_launch } from './backlog-drive-launch'
29
30
  import { backlog_drive_named } from './backlog-drive-named'
@@ -32,6 +33,7 @@ import { backlog_drive_offer_argv } from './backlog-drive-offer-argv'
32
33
  import { backlog_drive_owner } from './backlog-drive-owner'
33
34
  import { backlog_drive_restore } from './backlog-drive-restore'
34
35
  import { backlog_drive_retrospective } from './backlog-drive-retrospective'
36
+ import { backlog_drive_seats } from './backlog-drive-seats'
35
37
  import { backlog_offer_cli } from './backlog-offer-cli'
36
38
  import { backlog_ready } from './backlog-ready'
37
39
 
@@ -153,10 +155,10 @@ function finished_checker(seeded: ReadonlyArray<string>): (issue: string) => boo
153
155
  }
154
156
  }
155
157
 
156
- async function open_lanes(): Promise<ReadonlyArray<string>> {
158
+ async function open_lanes(): Promise<ReadonlyArray<LaneInfo>> {
157
159
  const lanes = await lane_registry.list_lanes()
158
160
 
159
- return lanes.filter((lane) => !lane.is_stranded).map((lane) => lane.issue)
161
+ return lanes.filter((lane) => !lane.is_stranded)
160
162
  }
161
163
 
162
164
  function end_line(end: DriveEnd): string {
@@ -190,6 +192,7 @@ function ports_of(
190
192
  offer: async (state) => await read_offer(state, context),
191
193
  launch: async (issue) => await backlog_drive_launch.launch(issue, context.owner),
192
194
  free_lanes: backlog_ready.drive_free_lane_count,
195
+ has_lane: backlog_drive_seats.has_kept_lane,
193
196
  judgement_owed: judgement_reader(),
194
197
  now: () => new Date(),
195
198
  sleep: async (milliseconds) => {
@@ -211,9 +214,11 @@ async function seeded_lanes(context: DriveContext): Promise<ReadonlyArray<string
211
214
 
212
215
  await backlog_drive_named.reconcile(carry, events, context.owner)
213
216
 
214
- const lanes = [...new Set([...(await open_lanes()), ...context.awaited])]
217
+ const open = await open_lanes()
218
+ const lanes = [...new Set([...open.map((lane) => lane.issue), ...context.awaited])]
219
+ const dispatched = await backlog_drive_carried.carried(open)
215
220
 
216
- return backlog_drive_restore.restore(lanes, events, carry.merged_issues ?? [])
221
+ return backlog_drive_restore.restore(lanes, events, carry.merged_issues ?? [], dispatched)
217
222
  }
218
223
 
219
224
  // The interval between two asks is `backlog:budget`'s own, so the driver keeps the one its reason prints.
@@ -1,5 +1,4 @@
1
- import { has_label_name, NEEDS_DECISION_LABEL } from '#scripts/issue/issue-labels'
2
- import { issue_state_cli } from '#scripts/issue/issue-state-cli'
1
+ import { lane_parked } from '#scripts/lane/lane-parked'
3
2
  import { run_carry, type RunCarry } from '#scripts/run/carry/run-carry'
4
3
  import type { RunEvent } from '#scripts/run/event/run-event-stream'
5
4
  import type { MergeResult } from '#scripts/run/merge/run-merge-cli'
@@ -13,7 +12,7 @@ const { MERGE_TOKEN } = run_merge_token
13
12
 
14
13
  const NO_RETRIES = 0
15
14
  const FIRST = 0
16
- const TERMINAL_OUTCOMES: ReadonlySet<string> = new Set(['merged', 'parked', 'split'])
15
+ const TERMINAL_OUTCOMES: ReadonlySet<string> = new Set(['merged', 'parked', 'split', 'withdrawn'])
17
16
 
18
17
  // The skip follows the declared order alone: an issue `run:add` put in was
19
18
  // asked for on its own, so its failure skips nothing and a declared failure does not skip it.
@@ -98,9 +97,8 @@ function skip_after_failure(target: string, issue: string, owner: string): void
98
97
  async function should_skip(issue: string, outcome: MergeResult['outcome']): Promise<boolean> {
99
98
  if (outcome === 'failed') return true
100
99
  if (outcome !== 'parked') return false
101
- const read = await issue_state_cli.read_issue(issue)
102
100
 
103
- return read.kind === 'state' && has_label_name(read.state.labels, NEEDS_DECISION_LABEL)
101
+ return await lane_parked.is_parked(issue)
104
102
  }
105
103
 
106
104
  async function mark_done(issue: string, result: MergeResult, owner: string): Promise<void> {
@@ -5,12 +5,14 @@ const SETTLED_KINDS: ReadonlySet<string> = new Set([
5
5
  run_event_stream.EVENT_KIND.MERGE,
6
6
  run_event_stream.EVENT_KIND.PARK,
7
7
  run_event_stream.EVENT_KIND.SPLIT,
8
+ run_event_stream.EVENT_KIND.WITHDRAW,
8
9
  run_event_stream.EVENT_KIND.OUTAGE,
9
10
  ])
10
11
  const DONE_KINDS: ReadonlySet<string> = new Set([
11
12
  run_event_stream.EVENT_KIND.MERGE,
12
13
  run_event_stream.EVENT_KIND.PARK,
13
14
  run_event_stream.EVENT_KIND.SPLIT,
15
+ run_event_stream.EVENT_KIND.WITHDRAW,
14
16
  ])
15
17
 
16
18
  function issue_of(event: RunEvent): string | undefined {
@@ -54,14 +56,38 @@ function parked_issues(events: ReadonlyArray<RunEvent>): ReadonlySet<number> {
54
56
  )
55
57
  }
56
58
 
59
+ // The issues this invocation launched or settled — any other dispatched lane was carried in from an
60
+ // earlier invocation.
61
+ function mentioned_issues(events: ReadonlyArray<RunEvent>): ReadonlySet<string> {
62
+ const tracked = events.filter(
63
+ (event) =>
64
+ event.kind === run_event_stream.EVENT_KIND.CHILD_LAUNCH || SETTLED_KINDS.has(event.kind),
65
+ )
66
+
67
+ return new Set(tracked.map((event) => issue_of(event)).filter((issue) => issue !== undefined))
68
+ }
69
+
70
+ // The lanes still in flight: launched by this invocation and not settled, or dispatched by an earlier
71
+ // one and never seen by this one — a lane nobody collects otherwise waits forever.
57
72
  function restore(
58
73
  lanes: ReadonlyArray<string>,
59
74
  events: ReadonlyArray<RunEvent>,
60
75
  merged_issues: ReadonlyArray<number>,
76
+ dispatched: ReadonlyArray<string> = [],
61
77
  ): ReadonlyArray<string> {
62
78
  const active = active_issues(events)
79
+ const mentioned = mentioned_issues(events)
80
+ const carried = new Set(dispatched.filter((issue) => !mentioned.has(issue)))
63
81
 
64
- return lanes.filter((issue) => active.has(issue) && !merged_issues.includes(Number(issue)))
82
+ return lanes.filter(
83
+ (issue) => (active.has(issue) || carried.has(issue)) && !merged_issues.includes(Number(issue)),
84
+ )
65
85
  }
66
86
 
67
- export const backlog_drive_restore = { active_issues, restore, settled_issues, parked_issues }
87
+ export const backlog_drive_restore = {
88
+ active_issues,
89
+ mentioned_issues,
90
+ restore,
91
+ settled_issues,
92
+ parked_issues,
93
+ }
@@ -0,0 +1,27 @@
1
+ import { lane_registry } from '#scripts/lane/lane-registry'
2
+ import type { DrivePorts } from './backlog-drive'
3
+
4
+ type SeatPorts = Pick<DrivePorts, 'free_lanes' | 'has_lane'>
5
+
6
+ // The issues a pass launches: every one that already holds a kept lane, which it reuses, then as many
7
+ // of the rest as there are free lanes.
8
+ async function seated(issues: ReadonlyArray<string>, ports: SeatPorts): Promise<Array<string>> {
9
+ const kept = await Promise.all(issues.map(async (issue) => await ports.has_lane(issue)))
10
+ const fresh = issues.filter((_issue, index) => kept[index] !== true)
11
+
12
+ return [
13
+ ...issues.filter((_issue, index) => kept[index] === true),
14
+ ...fresh.slice(0, await ports.free_lanes()),
15
+ ]
16
+ }
17
+
18
+ // A kept lane is one a child was dispatched into — `lane:launch` reuses it rather than opening one.
19
+ async function has_kept_lane(issue: string): Promise<boolean> {
20
+ const lane = await lane_registry.find_open_lane(issue)
21
+
22
+ return lane?.output !== undefined
23
+ }
24
+
25
+ const backlog_drive_seats = { has_kept_lane, seated }
26
+
27
+ export { backlog_drive_seats }
@@ -20,6 +20,7 @@ import { issue_cite } from '#scripts/issue/issue-cite'
20
20
  import type { LaunchOutcome } from '#scripts/lane/lane-launch-cli'
21
21
  import type { MergeResult } from '#scripts/run/merge/run-merge-cli'
22
22
  import { run_merge_token, type MergeToken } from '#scripts/run/merge/run-merge-token'
23
+ import { backlog_drive_seats } from './backlog-drive-seats'
23
24
 
24
25
  const { MERGE_TOKEN } = run_merge_token
25
26
  const RUN_VERDICT = 'run'
@@ -124,6 +125,9 @@ interface DrivePorts {
124
125
  // The lanes free right now — `backlog_ready.drive_free_lane_count` in production, the limit less the
125
126
  // open lanes.
126
127
  free_lanes: () => Promise<number>
128
+ // Whether the issue already holds a kept lane (one a child was dispatched into) — it is reused, so it
129
+ // takes no free seat.
130
+ has_lane: (issue: string) => Promise<boolean>
127
131
  // How many open issues lacking `auto-ok` still owe a judgement — `auto_ok_judge_read.owed` in
128
132
  // production, `0` when the read failed.
129
133
  judgement_owed: () => Promise<number>
@@ -265,10 +269,10 @@ async function refused(
265
269
  return ended('launch', state, undefined, issue)
266
270
  }
267
271
 
268
- // **No more launches than free lanes**. `backlog:offer` lists every ready issue
269
- // without reading the lanes, so launching all of them overran `JOSH_LANE_LIMIT` until `lane:open` ran
270
- // out of seats. The free count is read once, before the first launch, because every launch here opens
271
- // a lane the count would otherwise have to be told about.
272
+ // **No more new lanes than free lanes**. `backlog:offer` lists every ready issue without reading the
273
+ // lanes, so launching all of them overran `JOSH_LANE_LIMIT` until `lane:open` ran out of seats. An issue
274
+ // that already holds a kept lane reuses it and takes no seat (`backlog-drive-seats.ts`); the rest launch
275
+ // up to the free count, read once before the first launch.
272
276
  //
273
277
  // **Only a launch is activity**: a pass that started nothing — every lane taken
274
278
  // — keeps `active` where it was, or the once-a-minute offer would restart the idle budget for good.
@@ -284,9 +288,10 @@ async function launch_all(
284
288
  ports: DrivePorts,
285
289
  ): Promise<PassResult> {
286
290
  const in_flight = [...state.in_flight]
287
- const free = await ports.free_lanes()
288
291
 
289
- for (const issue of issues.slice(0, free)) {
292
+ const launching = await backlog_drive_seats.seated(issues, ports)
293
+
294
+ for (const issue of launching) {
290
295
  // eslint-disable-next-line no-await-in-loop -- lanes launch in order and the first refusal ends the pass
291
296
  const outcome = await ports.launch(issue)
292
297
 
@@ -0,0 +1,56 @@
1
+ import type { LaneInfo } from '#scripts/lane/lane-registry'
2
+ import type { RunEvent } from '#scripts/run/event/run-event-stream'
3
+ import { run_invocation } from '#scripts/run/run-invocation'
4
+ import { backlog_drive_restore } from './backlog-drive-restore'
5
+
6
+ // The open lanes that hold a place under the lane limit — the one count `lane:limit`, the drive and
7
+ // `run:board` all subtract from it.
8
+ //
9
+ // **A lane nobody will come back to holds no place.** A `--only` run works its named list and never
10
+ // touches another lane, so a lane an earlier run left with no child in it would fill the limit for the
11
+ // whole run, and the run's own issues would wait on it for good (joshuafolkken/kit#3967). Under a `--only`
12
+ // run such a lane is not counted; its tree and branch stay, for the next run that names its issue.
13
+ //
14
+ // **Everything else keeps its place, so the count errs toward fewer free lanes.** A run that drains
15
+ // the pool may offer any open lane's issue again, a lane whose child still works is busy whoever owns
16
+ // it, and with no run there is nobody to judge ownership for. The read stays local — no issue label is
17
+ // read — because the count gates the network read behind it (`backlog-ready.ts`).
18
+
19
+ interface OccupiedRead {
20
+ // The live run's invocation, `undefined` when no run is live here.
21
+ invocation: string | undefined
22
+ // The live run's own events, for the issues it launched or settled; `undefined` where its scope
23
+ // could not be told, which leaves every open lane counted.
24
+ events: ReadonlyArray<RunEvent> | undefined
25
+ // The issues whose lane has no child working in it; read only for a `--only` run.
26
+ idle: (lanes: ReadonlyArray<LaneInfo>) => ReadonlyArray<string>
27
+ }
28
+
29
+ // The issues a `--only` run owns — those it names and those it launched or settled — or `undefined`
30
+ // where there is no such run, or its events could not be scoped to it.
31
+ function owned_issues(read: OccupiedRead): ReadonlySet<string> | undefined {
32
+ const { invocation, events } = read
33
+ const is_only = invocation !== undefined && run_invocation.has_only(invocation)
34
+
35
+ if (!is_only || events === undefined) return undefined
36
+
37
+ const named = (run_invocation.issue_numbers(invocation) ?? []).map(String)
38
+
39
+ return new Set([...named, ...backlog_drive_restore.mentioned_issues(events)])
40
+ }
41
+
42
+ function occupied_lanes(lanes: ReadonlyArray<LaneInfo>, read: OccupiedRead): Array<LaneInfo> {
43
+ const live = lanes.filter((lane) => !lane.is_stranded)
44
+ const owned = owned_issues(read)
45
+
46
+ if (owned === undefined) return live
47
+
48
+ const idle = new Set(read.idle(live))
49
+
50
+ return live.filter((lane) => owned.has(lane.issue) || !idle.has(lane.issue))
51
+ }
52
+
53
+ const backlog_occupied = { occupied_lanes }
54
+
55
+ export { backlog_occupied }
56
+ export type { OccupiedRead }