@joshuafolkken/kit 1.1784.0 → 1.2114.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 (690) hide show
  1. package/.claude/agents/reviewer-verification.md +19 -0
  2. package/.claude/agents/reviewer.md +19 -0
  3. package/.claude/settings.json +5 -6
  4. package/.claude/skills/dependency-update/SKILL.md +9 -10
  5. package/.claude/skills/workflow-commands/SKILL.md +6 -6
  6. package/.claude/skills/workflow-commands/background-commands.md +8 -5
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +3 -1
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +5 -2
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +2 -3
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +1 -1
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +3 -2
  13. package/.claude/skills/workflow-commands/backlogrun.md +1 -1
  14. package/.claude/skills/workflow-commands/chain-rule.md +41 -32
  15. package/.claude/skills/workflow-commands/delegation.md +6 -6
  16. package/.claude/skills/workflow-commands/entry-sequence.md +2 -2
  17. package/.claude/skills/workflow-commands/followup.md +7 -5
  18. package/.claude/skills/workflow-commands/fullrun-steps.md +6 -6
  19. package/.claude/skills/workflow-commands/fullrun.md +1 -1
  20. package/.claude/skills/workflow-commands/issue-comments.md +9 -4
  21. package/.claude/skills/workflow-commands/issue-fold-existing.md +2 -1
  22. package/.claude/skills/workflow-commands/kickoff.md +2 -2
  23. package/.claude/skills/workflow-commands/observation-filing.md +1 -1
  24. package/.claude/skills/workflow-commands/observation-ledger.md +6 -4
  25. package/.claude/skills/workflow-commands/pre-gate-cut.md +9 -7
  26. package/.claude/skills/workflow-commands/prerequisite.md +3 -2
  27. package/.claude/skills/workflow-commands/retrospective.md +2 -2
  28. package/.claude/skills/workflow-commands/split-assessment.md +7 -5
  29. package/.codex/hooks.json +5 -5
  30. package/.github/actions/setup-pnpm/action.yml +9 -16
  31. package/.github/dependabot.yml +16 -0
  32. package/.github/workflows/auto-tag.yml +45 -0
  33. package/.github/workflows/ci.yml +155 -15
  34. package/.github/workflows/dependabot-auto-merge.yml +63 -4
  35. package/.github/workflows/production.yml +8 -0
  36. package/.github/workflows/publish.yml +59 -45
  37. package/.vscode/settings.json +19 -0
  38. package/CLAUDE.md +5 -12
  39. package/SECURITY.md +2 -2
  40. package/dist/CLAUDE.md +5 -12
  41. package/dist/commands/document-read-cli.js +2 -1
  42. package/dist/commands/document-section-main.js +2 -1
  43. package/dist/commands/read-set-cli.js +194 -134
  44. package/dist/commands/run-prep-main.js +9694 -10173
  45. package/dist/commands/run-step-cli.js +22138 -18285
  46. package/dist/hooks/audit-provision.js +23 -5
  47. package/dist/hooks/chunk-3GKE653K.js +6204 -0
  48. package/dist/hooks/chunk-45CJJOO7.js +1243 -0
  49. package/dist/hooks/{chunk-CWMBGKYW.js → chunk-5PM2JRGV.js} +2153 -856
  50. package/dist/hooks/{chunk-WR2KQAYZ.js → chunk-7SIUNQXK.js} +1207 -1470
  51. package/dist/hooks/{chunk-O2YBIDSI.js → chunk-CQD4LJE6.js} +1682 -1130
  52. package/dist/hooks/{chunk-VWWCFGWK.js → chunk-OPSRGWFU.js} +1 -1
  53. package/dist/hooks/{chunk-XGBNSWEI.js → chunk-UJ3FPMMM.js} +132 -49
  54. package/dist/hooks/chunk-VVNYJWEI.js +30 -0
  55. package/dist/hooks/chunk-Z6DQJKH3.js +2806 -0
  56. package/dist/hooks/codex-hook-adapter.js +8 -8
  57. package/dist/hooks/format-edited.js +4 -5
  58. package/dist/hooks/inputs.json +415 -389
  59. package/dist/hooks/pretool-guard.js +7 -6
  60. package/dist/hooks/session-lang.js +101 -258
  61. package/dist/hooks/stop-guard.js +1744 -2576
  62. package/dist/josh.js +7133 -6368
  63. package/dist/version/index.js +161 -1
  64. package/eslint/rules/comment-why-only.js +108 -0
  65. package/package.json +15 -8
  66. package/prompts/coding-standards.md +1 -1
  67. package/prompts/collaboration-workflow/file-edits.md +14 -11
  68. package/prompts/collaboration-workflow/issue-citation.md +0 -4
  69. package/prompts/collaboration-workflow/issue-template.md +40 -21
  70. package/prompts/collaboration-workflow/lane-instructions.md +3 -1
  71. package/prompts/collaboration-workflow/measurement.md +8 -1
  72. package/prompts/collaboration-workflow/operating-rules.md +11 -2
  73. package/prompts/collaboration-workflow/output-bounds.md +6 -2
  74. package/prompts/collaboration-workflow/plan-comment.md +1 -1
  75. package/prompts/collaboration-workflow/principles.md +9 -0
  76. package/prompts/collaboration-workflow/residency.md +3 -2
  77. package/prompts/collaboration-workflow/rule-delivery.md +13 -1
  78. package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
  79. package/prompts/collaboration-workflow/wip-cap.md +3 -1
  80. package/prompts/review-rubric.md +8 -0
  81. package/prompts/review.md +31 -22
  82. package/prompts/testing-guide.md +1 -0
  83. package/scripts/agent/agent-argv.ts +14 -16
  84. package/scripts/agent/agent-provider.ts +138 -0
  85. package/scripts/agent/agent-role-profile.ts +108 -156
  86. package/scripts/agent/agent-safe-text.ts +20 -5
  87. package/scripts/agent/api-usage-limit.ts +65 -0
  88. package/scripts/agent/claude-agent-argv.ts +19 -9
  89. package/scripts/agent/codex-agent-argv.ts +118 -22
  90. package/scripts/agent/usage-limit-reset.ts +179 -0
  91. package/scripts/auth/claude-token-output.ts +2 -2
  92. package/scripts/auto-ok/auto-ok-author.ts +53 -0
  93. package/scripts/auto-ok/auto-ok-cli.ts +20 -9
  94. package/scripts/backlog/backlog-defect-priority.ts +1 -1
  95. package/scripts/backlog/backlog-drive-cli.ts +6 -1
  96. package/scripts/backlog/backlog-drive-hold.ts +3 -3
  97. package/scripts/backlog/backlog-drive-idle.ts +10 -0
  98. package/scripts/backlog/backlog-drive-launch-pass.ts +90 -0
  99. package/scripts/backlog/backlog-drive-launch.ts +8 -2
  100. package/scripts/backlog/backlog-drive-named-offer.ts +73 -30
  101. package/scripts/backlog/backlog-drive-named.ts +28 -7
  102. package/scripts/backlog/backlog-drive-paused.ts +5 -4
  103. package/scripts/backlog/backlog-drive-restore.ts +11 -3
  104. package/scripts/backlog/backlog-drive.ts +21 -60
  105. package/scripts/backlog/backlog-gate-scope.ts +61 -0
  106. package/scripts/backlog/backlog-measure-close.ts +112 -0
  107. package/scripts/backlog/backlog-next.ts +39 -49
  108. package/scripts/backlog/backlog-occupied.ts +2 -2
  109. package/scripts/backlog/backlog-overlap.ts +42 -8
  110. package/scripts/backlog/backlog-plan-cli.ts +4 -1
  111. package/scripts/backlog/backlog-plan-comment.ts +81 -0
  112. package/scripts/backlog/backlog-plan-read.ts +19 -6
  113. package/scripts/backlog/backlog-pool.ts +50 -12
  114. package/scripts/backlog/backlog-rank.ts +60 -13
  115. package/scripts/backlog/backlog-stalled-detect.ts +2 -1
  116. package/scripts/backlog/backlog-waves.ts +17 -2
  117. package/scripts/backlog/backlogrun-launch.ts +7 -4
  118. package/scripts/cost-runtime/context-cut-payback.ts +27 -25
  119. package/scripts/cost-runtime/cost-cli.ts +37 -16
  120. package/scripts/cost-runtime/cost-pricing.ts +9 -0
  121. package/scripts/cost-runtime/cost-verdict.ts +4 -1
  122. package/scripts/delegation/delegation-policy.ts +2 -2
  123. package/scripts/delegation/duplicate-read-guard.ts +1 -0
  124. package/scripts/delegation/investigation-guard.ts +14 -2
  125. package/scripts/delegation/investigation-reads.ts +45 -23
  126. package/scripts/dependabot/dependabot-cooldown.ts +3 -2
  127. package/scripts/discovery/repo-party.ts +80 -20
  128. package/scripts/discovery/session-owner-pin.ts +51 -0
  129. package/scripts/document/capped-print.ts +1 -1
  130. package/scripts/document/command-reference-line-budget.ts +42 -0
  131. package/scripts/document/document-byte-budget.ts +17 -7
  132. package/scripts/document/document-checks.ts +20 -0
  133. package/scripts/document/document-ratchet-check.ts +101 -0
  134. package/scripts/document/document-section.ts +1 -0
  135. package/scripts/document/entry-read-budget.ts +7 -3
  136. package/scripts/document/issue-citation-budget.ts +94 -0
  137. package/scripts/epic/epic-cli-argv.ts +2 -2
  138. package/scripts/epic/epic-fetch.ts +4 -2
  139. package/scripts/epic/epic-graph.ts +7 -2
  140. package/scripts/epic/epic-issue.ts +3 -1
  141. package/scripts/epic/epic-rank.ts +5 -0
  142. package/scripts/exports/unused-members-check.ts +62 -0
  143. package/scripts/exports/unused-members-cli.ts +6 -51
  144. package/scripts/followup/followup-ci-path.ts +67 -0
  145. package/scripts/followup/followup-coderabbit-retrigger.ts +261 -0
  146. package/scripts/followup/followup-coderabbit-wait.ts +13 -10
  147. package/scripts/followup/followup-failed-tests.ts +160 -0
  148. package/scripts/followup/followup-job-needs.ts +100 -0
  149. package/scripts/followup/followup-job-rerun.ts +303 -0
  150. package/scripts/followup/followup-main-move.ts +113 -0
  151. package/scripts/followup/git-followup-finish.ts +43 -11
  152. package/scripts/followup/git-followup-pending.ts +3 -3
  153. package/scripts/followup/git-followup-stages.ts +41 -11
  154. package/scripts/followup/git-pr-followup-checks.ts +55 -16
  155. package/scripts/followup/git-pr-followup-wrapup.ts +24 -5
  156. package/scripts/followup/git-pr-followup.ts +62 -27
  157. package/scripts/fresh-machine/fresh-machine-script.ts +5 -9
  158. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +2 -2
  159. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +2 -2
  160. package/scripts/gate/busy-average.ts +34 -2
  161. package/scripts/gate/check-related-pinned.ts +43 -0
  162. package/scripts/gate/check-related-reuse.ts +64 -0
  163. package/scripts/gate/check-related-section.ts +57 -0
  164. package/scripts/gate/check-related-unplanned.ts +34 -0
  165. package/scripts/gate/check-related.ts +265 -0
  166. package/scripts/gate/command-memory.ts +1 -1
  167. package/scripts/gate/core-admission.ts +24 -6
  168. package/scripts/gate/core-budget-timing.ts +20 -6
  169. package/scripts/gate/core-budget.ts +40 -11
  170. package/scripts/gate/core-wait-record.ts +3 -2
  171. package/scripts/gate/core-wait.ts +25 -2
  172. package/scripts/gate/core-weights.ts +15 -7
  173. package/scripts/gate/gate-labels.ts +46 -0
  174. package/scripts/gate/gate-ledger.ts +14 -6
  175. package/scripts/gate/gate-plan.ts +103 -58
  176. package/scripts/gate/gate-report.ts +9 -7
  177. package/scripts/gate/gate-run-steps.ts +26 -3
  178. package/scripts/gate/gate-skip.ts +45 -6
  179. package/scripts/gate/gate-static-group.ts +60 -0
  180. package/scripts/gate/gate-steps.ts +2 -1
  181. package/scripts/gate/gate-tree.ts +4 -2
  182. package/scripts/gate/hook-gate-reuse.ts +19 -2
  183. package/scripts/gate/metrics-totals.ts +59 -0
  184. package/scripts/gate/scoped-green.ts +68 -15
  185. package/scripts/gate/since-green.ts +14 -6
  186. package/scripts/gate/structural-checks.ts +96 -0
  187. package/scripts/gate/verification-gate.ts +51 -16
  188. package/scripts/gate/verification-inputs.ts +21 -6
  189. package/scripts/gate/verification-tail.ts +52 -0
  190. package/scripts/gh/gh-author-association.ts +23 -2
  191. package/scripts/gh/git-ai-review-scan.ts +1 -1
  192. package/scripts/gh/git-gh-api-path.ts +12 -3
  193. package/scripts/gh/git-gh-comments-unreadable.ts +36 -0
  194. package/scripts/gh/git-gh-exec.ts +32 -2
  195. package/scripts/gh/git-gh-issue-read.ts +4 -2
  196. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  197. package/scripts/gh/git-gh-issue-write.ts +8 -2
  198. package/scripts/gh/git-gh-issue.ts +12 -5
  199. package/scripts/gh/git-gh-pr-auto-merge.ts +13 -6
  200. package/scripts/gh/git-gh-pr-graphql-state.ts +56 -0
  201. package/scripts/gh/git-gh-pr-read.ts +74 -13
  202. package/scripts/gh/git-gh-pr-rest.ts +3 -1
  203. package/scripts/gh/git-gh-pr-snapshot.ts +22 -7
  204. package/scripts/gh/git-gh-pr-too-large.ts +20 -0
  205. package/scripts/gh/git-gh-pr.ts +21 -7
  206. package/scripts/gh/git-gh-retry.ts +141 -0
  207. package/scripts/gh/git-pr-ai-review.ts +15 -6
  208. package/scripts/gh/git-pr-checks-eval.ts +3 -0
  209. package/scripts/gh/git-pr-checks.ts +5 -5
  210. package/scripts/git/base-tree.ts +30 -0
  211. package/scripts/git/change-base.ts +4 -10
  212. package/scripts/git/changed-paths-sync.ts +63 -0
  213. package/scripts/git/changed-paths.ts +16 -1
  214. package/scripts/git/git-common-directory.ts +11 -1
  215. package/scripts/git/git-diff-reads.ts +66 -20
  216. package/scripts/git/git-error.ts +9 -2
  217. package/scripts/git/git-schemas.ts +13 -0
  218. package/scripts/git/git-worktree.ts +13 -0
  219. package/scripts/git/gone-branch.ts +4 -1
  220. package/scripts/git/main-merge-budget-conflict.ts +89 -0
  221. package/scripts/git/main-merge-guard.ts +55 -18
  222. package/scripts/git/main-merge-regenerate.ts +59 -25
  223. package/scripts/git/main-merge.ts +53 -5
  224. package/scripts/git/no-base-checkout.ts +127 -0
  225. package/scripts/git/stash/git-stash.ts +11 -2
  226. package/scripts/git/stash/stash-push-cli.ts +10 -0
  227. package/scripts/hooks/batch-guard.ts +40 -21
  228. package/scripts/hooks/edited-cspell.ts +37 -11
  229. package/scripts/hooks/format-edited-file.ts +22 -2
  230. package/scripts/hooks/lane-plan-guard.ts +50 -0
  231. package/scripts/hooks/pre-commit-lint-switch.ts +9 -0
  232. package/scripts/hooks/pre-commit-lint.ts +14 -1
  233. package/scripts/hooks/pre-push-related-switch.ts +10 -0
  234. package/scripts/hooks/pre-push-unit.ts +76 -10
  235. package/scripts/hooks/pretool-guard.ts +2 -0
  236. package/scripts/hooks/reserved-run.ts +14 -1
  237. package/scripts/hooks/run-hook.sh +23 -8
  238. package/scripts/hooks/step-zero-notice.ts +6 -1
  239. package/scripts/hooks/stop-actions.ts +3 -15
  240. package/scripts/hooks/stop-guard.ts +7 -6
  241. package/scripts/init/distributed-paths.ts +1 -0
  242. package/scripts/init/hook-command-rewrite.ts +5 -1
  243. package/scripts/init/hook-launch.ts +4 -2
  244. package/scripts/issue/aggregation-split.ts +52 -0
  245. package/scripts/issue/defect-origin-line.ts +19 -1
  246. package/scripts/issue/defect-rate-cli.ts +5 -102
  247. package/scripts/issue/defect-report.ts +4 -4
  248. package/scripts/issue/effect-audit-cli.ts +178 -0
  249. package/scripts/issue/effect-audit.ts +185 -0
  250. package/scripts/issue/expected-effect-lint.ts +64 -9
  251. package/scripts/issue/git-next-issues.ts +5 -0
  252. package/scripts/issue/issue-body-or-comment.ts +38 -4
  253. package/scripts/issue/issue-cite-cli.ts +1 -3
  254. package/scripts/issue/issue-classification.ts +1 -0
  255. package/scripts/issue/issue-closing-pr.ts +8 -1
  256. package/scripts/issue/issue-evidence-exit.ts +4 -2
  257. package/scripts/issue/issue-file-cap.ts +58 -0
  258. package/scripts/issue/issue-file-cli.ts +111 -38
  259. package/scripts/issue/issue-file-deferral.ts +52 -0
  260. package/scripts/issue/issue-file-fold.ts +29 -10
  261. package/scripts/issue/issue-file-interrupt.ts +85 -0
  262. package/scripts/issue/issue-file-lane-hint.ts +30 -0
  263. package/scripts/issue/issue-file.ts +43 -6
  264. package/scripts/issue/issue-labels.ts +19 -2
  265. package/scripts/issue/issue-lint-cli.ts +27 -2
  266. package/scripts/issue/issue-measured-cli.ts +13 -35
  267. package/scripts/issue/issue-measured-close.ts +58 -0
  268. package/scripts/issue/issue-not-before-label.ts +2 -2
  269. package/scripts/issue/issue-not-before.ts +4 -1
  270. package/scripts/issue/issue-priority-cli.ts +60 -0
  271. package/scripts/issue/issue-priority-raise.ts +55 -0
  272. package/scripts/issue/issue-priority.ts +174 -43
  273. package/scripts/issue/issue-rank.ts +21 -4
  274. package/scripts/issue/issue-read-cli.ts +32 -6
  275. package/scripts/issue/issue-read.ts +37 -12
  276. package/scripts/issue/issue-sample-gate.ts +95 -74
  277. package/scripts/issue/issue-sample-prime.ts +127 -0
  278. package/scripts/issue/issue-sample-required.ts +57 -0
  279. package/scripts/issue/issue-sample-source.ts +12 -11
  280. package/scripts/issue/issue-search.ts +124 -0
  281. package/scripts/issue/issue-state.ts +13 -1
  282. package/scripts/issue/issue-template-section.ts +43 -0
  283. package/scripts/issue/issue-watch-diff.ts +31 -1
  284. package/scripts/issue/issue-watch-hook.ts +11 -2
  285. package/scripts/issue/issue-watch.ts +55 -12
  286. package/scripts/issue/markdown-section.ts +1 -0
  287. package/scripts/issue/measure-declaration.ts +121 -0
  288. package/scripts/issue/measure-rerun-cli.ts +7 -70
  289. package/scripts/issue/plan-consumers-section.ts +22 -1
  290. package/scripts/issue/reproduction-measure.ts +3 -1
  291. package/scripts/issue/size-estimate.ts +90 -0
  292. package/scripts/issue/trusted-measure.ts +84 -0
  293. package/scripts/josh/command-usage.ts +3 -2
  294. package/scripts/josh/durable-state-migrate.ts +105 -0
  295. package/scripts/josh/durable-state.ts +112 -0
  296. package/scripts/josh/josh-aliases.ts +95 -0
  297. package/scripts/josh/josh-command-map.ts +1 -82
  298. package/scripts/josh/josh-commands-agent-hooks.ts +6 -6
  299. package/scripts/josh/josh-commands-ai.ts +5 -5
  300. package/scripts/josh/josh-commands-development.ts +59 -58
  301. package/scripts/josh/josh-commands-issue-write.ts +23 -4
  302. package/scripts/josh/josh-commands-issue.ts +32 -0
  303. package/scripts/josh/josh-commands-lane.ts +87 -4
  304. package/scripts/josh/josh-commands-lint.ts +16 -3
  305. package/scripts/josh/josh-commands-maintenance.ts +15 -0
  306. package/scripts/josh/josh-commands-review.ts +41 -6
  307. package/scripts/josh/josh-commands-ship.ts +56 -8
  308. package/scripts/josh/josh-commands-test-check.ts +80 -0
  309. package/scripts/josh/josh-commands-workflow.ts +1 -1
  310. package/scripts/josh/josh-environment-file.ts +43 -3
  311. package/scripts/josh/josh-in-process.ts +2 -2
  312. package/scripts/josh/josh-logic.ts +5 -3
  313. package/scripts/josh/josh-run.ts +19 -1
  314. package/scripts/josh/run-record-path.ts +15 -0
  315. package/scripts/josh/stamp-file.ts +11 -2
  316. package/scripts/lane/lane-adopt-cli.ts +98 -0
  317. package/scripts/lane/lane-adopt.ts +208 -0
  318. package/scripts/lane/lane-await.ts +5 -8
  319. package/scripts/lane/lane-baseline.ts +123 -0
  320. package/scripts/lane/lane-breakdown-cli.ts +64 -0
  321. package/scripts/lane/lane-breakdown-implement.ts +170 -0
  322. package/scripts/lane/lane-breakdown-laps.ts +107 -0
  323. package/scripts/lane/lane-breakdown-pinned.ts +76 -0
  324. package/scripts/lane/lane-breakdown-text.ts +145 -0
  325. package/scripts/lane/lane-breakdown.ts +239 -0
  326. package/scripts/lane/lane-check-stats.ts +29 -8
  327. package/scripts/lane/lane-child-invocation.ts +9 -2
  328. package/scripts/lane/lane-child-mark-keys.ts +12 -0
  329. package/scripts/lane/lane-child-marker.ts +20 -1
  330. package/scripts/lane/lane-ci-path-stats.ts +73 -0
  331. package/scripts/lane/lane-claim-stats.ts +84 -0
  332. package/scripts/lane/lane-close.ts +8 -3
  333. package/scripts/lane/lane-dispatch-log.ts +3 -3
  334. package/scripts/lane/lane-dispatch.ts +2 -2
  335. package/scripts/lane/lane-gap-stats.ts +7 -6
  336. package/scripts/lane/lane-guard-policy.ts +7 -1
  337. package/scripts/lane/lane-implement-stats.ts +207 -0
  338. package/scripts/lane/lane-implement-subagents.ts +90 -0
  339. package/scripts/lane/lane-investigation-switch.ts +16 -0
  340. package/scripts/lane/lane-ledger-kind.ts +31 -0
  341. package/scripts/lane/lane-ledger-schema-ship.ts +49 -0
  342. package/scripts/lane/lane-ledger-schema.ts +119 -36
  343. package/scripts/lane/lane-ledger-write.ts +257 -0
  344. package/scripts/lane/lane-ledger.ts +31 -177
  345. package/scripts/lane/lane-load.ts +1 -1
  346. package/scripts/lane/lane-measure-cli.ts +176 -59
  347. package/scripts/lane/lane-net-read.ts +44 -0
  348. package/scripts/lane/lane-net-time.ts +212 -0
  349. package/scripts/lane/lane-open.ts +16 -7
  350. package/scripts/lane/lane-output.ts +2 -4
  351. package/scripts/lane/lane-overrun-prompt.ts +147 -0
  352. package/scripts/lane/lane-overrun.ts +274 -0
  353. package/scripts/lane/lane-paths.ts +24 -1
  354. package/scripts/lane/lane-pause-cli.ts +4 -1
  355. package/scripts/lane/lane-paused-spans.ts +97 -0
  356. package/scripts/lane/lane-plan-check.ts +207 -0
  357. package/scripts/lane/lane-plan-cli.ts +41 -0
  358. package/scripts/lane/lane-plan.ts +144 -0
  359. package/scripts/lane/lane-registry.ts +14 -1
  360. package/scripts/lane/lane-relaunch.ts +1 -1
  361. package/scripts/lane/lane-round-one-stats.ts +51 -0
  362. package/scripts/lane/lane-sampler.ts +23 -4
  363. package/scripts/lane/lane-session-action.ts +93 -18
  364. package/scripts/lane/lane-session-broker.ts +49 -21
  365. package/scripts/lane/lane-session-reread-main.ts +14 -0
  366. package/scripts/lane/lane-session-reread.ts +51 -0
  367. package/scripts/lane/lane-ship-lap-stats.ts +74 -0
  368. package/scripts/lane/lane-sleep.ts +58 -0
  369. package/scripts/lane/lane-spent.ts +1 -1
  370. package/scripts/lane/lane-split-stats.ts +28 -0
  371. package/scripts/lane/lane-stage-stats.ts +13 -1
  372. package/scripts/lane/lane-state.ts +41 -9
  373. package/scripts/lane/lane-stats-tables.ts +58 -0
  374. package/scripts/lane/lane-stats.ts +21 -5
  375. package/scripts/lane/lane-stop-stats.ts +51 -19
  376. package/scripts/lane/lane-tail-stats.ts +32 -0
  377. package/scripts/lane/lane-transcript-calls.ts +67 -10
  378. package/scripts/lane/lane-transcript-stats.ts +44 -12
  379. package/scripts/lane/lane-value-cell.ts +72 -0
  380. package/scripts/lane/lane-value.ts +118 -0
  381. package/scripts/lane/lane-withdraw-closed.ts +124 -0
  382. package/scripts/lane/lane-withdraw.ts +108 -31
  383. package/scripts/lane/lane-yield.ts +85 -0
  384. package/scripts/lane/openai-lane-ship.ts +12 -11
  385. package/scripts/lane/openai-lane-supervisor-decision.ts +6 -8
  386. package/scripts/lane/openai-lane-supervisor-owner.ts +5 -8
  387. package/scripts/lane/openai-lane-supervisor.ts +5 -4
  388. package/scripts/lib/poll.ts +38 -9
  389. package/scripts/lib/settled-together.ts +10 -0
  390. package/scripts/lib/shell-quote.ts +21 -0
  391. package/scripts/lib/token-shape.ts +111 -0
  392. package/scripts/lint/eslint-daemon-sources.ts +79 -0
  393. package/scripts/lint/eslint-daemon.ts +45 -6
  394. package/scripts/lint/eslint-warm-cli.ts +7 -0
  395. package/scripts/lint/eslint-warm.ts +66 -0
  396. package/scripts/lint/import-graph.ts +141 -11
  397. package/scripts/lint/lint-gate-scope.ts +2 -2
  398. package/scripts/lint/lint-parallel.ts +54 -6
  399. package/scripts/lint/lint-related.ts +154 -33
  400. package/scripts/lint/rename-cli.ts +179 -0
  401. package/scripts/lint/rename-identifier.ts +192 -0
  402. package/scripts/lint/type-only-imports.ts +58 -0
  403. package/scripts/notify/telegram-notify.ts +12 -3
  404. package/scripts/observations/observation-record.ts +42 -16
  405. package/scripts/observations/observations-flush.ts +32 -9
  406. package/scripts/overrides/overrides-check.ts +9 -1
  407. package/scripts/release/release-approval-prune.ts +7 -1
  408. package/scripts/repo/required-checks-logic.ts +4 -1
  409. package/scripts/repo/ruleset-check.ts +4 -3
  410. package/scripts/report/markdown-shape.ts +28 -0
  411. package/scripts/report/report-format-reference.ts +15 -1
  412. package/scripts/report/report-labels.ts +12 -1
  413. package/scripts/report/report-lint-child-summary.ts +94 -0
  414. package/scripts/report/report-lint-cli.ts +26 -7
  415. package/scripts/report/report-lint.ts +2 -20
  416. package/scripts/review/prose-stale-cli.ts +88 -0
  417. package/scripts/review/prose-stale-scan.ts +152 -0
  418. package/scripts/review/review-attest.ts +73 -23
  419. package/scripts/review/review-brief-cli.ts +68 -19
  420. package/scripts/review/review-brief-paths.ts +22 -0
  421. package/scripts/review/review-brief.ts +14 -7
  422. package/scripts/review/review-comment-carry.ts +47 -0
  423. package/scripts/review/review-comment-round.ts +85 -22
  424. package/scripts/review/review-finding-ledger.ts +109 -7
  425. package/scripts/review/review-finding-text.ts +43 -0
  426. package/scripts/review/review-interaction.ts +11 -0
  427. package/scripts/review/review-lane-handover.ts +68 -0
  428. package/scripts/review/review-lint-cli.ts +57 -0
  429. package/scripts/review/review-lint-findings.ts +92 -0
  430. package/scripts/review/review-lint-sections.ts +80 -0
  431. package/scripts/review/review-lint.ts +221 -0
  432. package/scripts/review/review-prior-findings.ts +102 -0
  433. package/scripts/review/review-readers-scan.ts +4 -2
  434. package/scripts/review/review-record-cli.ts +177 -54
  435. package/scripts/review/review-record-code-round.ts +55 -0
  436. package/scripts/review/review-record-comment.ts +60 -0
  437. package/scripts/review/review-record-interaction.ts +35 -0
  438. package/scripts/review/review-record-pinned.ts +108 -0
  439. package/scripts/review/review-record.ts +25 -1
  440. package/scripts/review/review-reviewed-tree.ts +307 -0
  441. package/scripts/review/review-round-one-closure.ts +62 -0
  442. package/scripts/review/review-round-reuse.ts +106 -0
  443. package/scripts/review/review-round2-cli.ts +91 -29
  444. package/scripts/review/review-round2-skip-comment.ts +55 -0
  445. package/scripts/review/review-round2.ts +24 -8
  446. package/scripts/review/review-stamps.ts +7 -3
  447. package/scripts/review/review-stop-log.ts +20 -6
  448. package/scripts/review/review-stops-cli.ts +2 -1
  449. package/scripts/review/review-submit-cli.ts +101 -0
  450. package/scripts/rules/agent-model.ts +1 -1
  451. package/scripts/rules/bash-triggers.ts +3 -3
  452. package/scripts/rules/concise-reply.ts +88 -0
  453. package/scripts/rules/credential-body.ts +77 -0
  454. package/scripts/rules/decision-oracle.ts +1 -1
  455. package/scripts/rules/delivered-rules.ts +36 -17
  456. package/scripts/rules/destructive-command.ts +3 -11
  457. package/scripts/rules/direct-issue-comment.ts +34 -12
  458. package/scripts/rules/direct-issue-read.ts +191 -0
  459. package/scripts/rules/direct-pr-create.ts +4 -24
  460. package/scripts/rules/early-heartbeat.ts +2 -0
  461. package/scripts/rules/edit-tool.ts +27 -0
  462. package/scripts/rules/file-edit-rows.ts +17 -0
  463. package/scripts/rules/file-rewrite.ts +146 -0
  464. package/scripts/rules/firing-point.ts +3 -2
  465. package/scripts/rules/gh-api.ts +128 -7
  466. package/scripts/rules/gh-repo-selector.ts +84 -0
  467. package/scripts/rules/git-force.ts +2 -2
  468. package/scripts/rules/guard-remedy.ts +14 -5
  469. package/scripts/rules/implementation-cut.ts +61 -28
  470. package/scripts/rules/investigator-foreground.ts +231 -0
  471. package/scripts/rules/issue-citation.ts +15 -60
  472. package/scripts/rules/issue-comments.ts +9 -4
  473. package/scripts/rules/josh-git-bare.ts +1 -1
  474. package/scripts/rules/lane-background.ts +41 -13
  475. package/scripts/rules/lane-direct-tool.ts +107 -9
  476. package/scripts/rules/lane-interactive-ask.ts +4 -1
  477. package/scripts/rules/lane-park.ts +4 -2
  478. package/scripts/rules/lane-spilled-read.ts +90 -0
  479. package/scripts/rules/lane-split-park.ts +1 -1
  480. package/scripts/rules/lane-switch-main.ts +4 -4
  481. package/scripts/rules/last-prompt.ts +20 -47
  482. package/scripts/rules/oracle-consulted.ts +1 -1
  483. package/scripts/rules/permission-guards.ts +5 -1
  484. package/scripts/rules/piped-verification-shapes.ts +110 -0
  485. package/scripts/rules/piped-verification.ts +67 -17
  486. package/scripts/rules/poll-loop.ts +72 -9
  487. package/scripts/rules/pr-merge.ts +5 -5
  488. package/scripts/rules/pre-gate-cut.ts +3 -3
  489. package/scripts/rules/protected-files.ts +88 -24
  490. package/scripts/rules/reply-language.ts +3 -17
  491. package/scripts/rules/reply-prose.ts +34 -0
  492. package/scripts/rules/repo-admin.ts +132 -0
  493. package/scripts/rules/reviewer-foreground.ts +88 -0
  494. package/scripts/rules/round-one-check-related.ts +120 -0
  495. package/scripts/rules/rule-body-guard.ts +5 -15
  496. package/scripts/rules/rule-value-cache.ts +53 -5
  497. package/scripts/rules/rule-value-candidates.ts +84 -0
  498. package/scripts/rules/rule-value-cli.ts +59 -11
  499. package/scripts/rules/rule-value.ts +17 -4
  500. package/scripts/rules/run-tail-rule.ts +1 -1
  501. package/scripts/rules/sed-in-place.ts +196 -0
  502. package/scripts/rules/shell-segments.ts +10 -2
  503. package/scripts/rules/stop-rule-rows.ts +1 -13
  504. package/scripts/rules/stop-rules.ts +33 -58
  505. package/scripts/rules/test-declared-commit.ts +1 -1
  506. package/scripts/rules/third-party-write.ts +110 -23
  507. package/scripts/rules/timed-rerun.ts +1 -1
  508. package/scripts/rules/turn-filings.ts +90 -0
  509. package/scripts/rules/worktree-guard.ts +22 -7
  510. package/scripts/run/board/run-board-ending-reason.ts +6 -2
  511. package/scripts/run/board/run-board-fit.ts +24 -1
  512. package/scripts/run/board/run-board-found.ts +134 -0
  513. package/scripts/run/board/run-board-header.ts +8 -0
  514. package/scripts/run/board/run-board-labels.ts +11 -1
  515. package/scripts/run/board/run-board-lane-time.ts +1 -0
  516. package/scripts/run/board/run-board-link.ts +15 -2
  517. package/scripts/run/board/run-board-notes.ts +7 -1
  518. package/scripts/run/board/run-board-operation.ts +4 -2
  519. package/scripts/run/board/run-board-phase.ts +2 -5
  520. package/scripts/run/board/run-board-plan.ts +1 -1
  521. package/scripts/run/board/run-board-read.ts +46 -1
  522. package/scripts/run/board/run-board-reason.ts +20 -8
  523. package/scripts/run/board/run-board-render-checks.ts +3 -2
  524. package/scripts/run/board/run-board-render-drawn.ts +11 -0
  525. package/scripts/run/board/run-board-render-found.ts +157 -0
  526. package/scripts/run/board/run-board-render-lead.ts +9 -0
  527. package/scripts/run/board/run-board-render-legend.ts +19 -0
  528. package/scripts/run/board/run-board-render-tail.ts +1 -1
  529. package/scripts/run/board/run-board-render.ts +82 -18
  530. package/scripts/run/board/run-board-spin.ts +1 -3
  531. package/scripts/run/board/run-board-status.ts +2 -0
  532. package/scripts/run/board/run-board-tick.ts +39 -10
  533. package/scripts/run/carry/run-carry-ended.ts +3 -2
  534. package/scripts/run/carry/run-carry.ts +3 -2
  535. package/scripts/run/cut/run-cut-cli.ts +33 -133
  536. package/scripts/run/cut/run-cut-digest.ts +152 -0
  537. package/scripts/run/cut/run-cut-handoff.ts +52 -8
  538. package/scripts/run/cut/run-cut-report.ts +24 -3
  539. package/scripts/run/cut/run-cut-resume.ts +129 -0
  540. package/scripts/run/cut/run-cut-review.ts +105 -0
  541. package/scripts/run/cut/run-cut.ts +18 -7
  542. package/scripts/run/detached-launch.ts +1 -1
  543. package/scripts/run/entry/run-entry-cli.ts +88 -19
  544. package/scripts/run/entry/run-entry-spill.ts +83 -0
  545. package/scripts/run/entry/run-entry-stop.ts +5 -1
  546. package/scripts/run/entry/run-entry-trust.ts +91 -0
  547. package/scripts/run/event/run-event-baseline.ts +56 -0
  548. package/scripts/run/event/run-event-filed.ts +9 -3
  549. package/scripts/run/event/run-event-overrun.ts +197 -0
  550. package/scripts/run/event/run-event-render.ts +4 -0
  551. package/scripts/run/event/run-event-stream.ts +25 -3
  552. package/scripts/run/hold/run-stop-notify.ts +3 -8
  553. package/scripts/run/merge/run-merge-cap.ts +57 -0
  554. package/scripts/run/merge/run-merge-cli.ts +66 -23
  555. package/scripts/run/merge/run-merge-outcome.ts +2 -0
  556. package/scripts/run/merge/run-merge-ship.ts +13 -3
  557. package/scripts/run/merge/run-merge-signals.ts +40 -23
  558. package/scripts/run/merge/run-merge-steps.ts +56 -36
  559. package/scripts/run/merge/run-merge-usage-limit.ts +53 -0
  560. package/scripts/run/merge/run-merge-withdrawn.ts +53 -0
  561. package/scripts/run/merge/run-merge.ts +41 -14
  562. package/scripts/run/review/run-review-cli.ts +306 -20
  563. package/scripts/run/review/run-review-early-push.ts +121 -0
  564. package/scripts/run/review/run-review-join-read.ts +94 -0
  565. package/scripts/run/review/run-review-pinned-accepted.ts +126 -0
  566. package/scripts/run/review/run-review-pinned-join.ts +216 -0
  567. package/scripts/run/review/run-review-pinned-steps.ts +383 -0
  568. package/scripts/run/review/run-review-pinned-tree.ts +37 -0
  569. package/scripts/run/review/run-review-pinned.ts +53 -0
  570. package/scripts/run/review/run-review-steps.ts +70 -34
  571. package/scripts/run/review/run-review.ts +26 -6
  572. package/scripts/run/run-parent-cut-hook.ts +3 -4
  573. package/scripts/run/run-prep.ts +1 -0
  574. package/scripts/run/run-stage-read.ts +2 -6
  575. package/scripts/run/run-step-cli.ts +17 -5
  576. package/scripts/run/run-step-lane-plan.ts +27 -0
  577. package/scripts/run/run-step-section.ts +26 -7
  578. package/scripts/run/run-step.ts +5 -2
  579. package/scripts/run/ship/run-ship-args.ts +19 -0
  580. package/scripts/run/ship/run-ship-brokered-review.ts +106 -0
  581. package/scripts/run/ship/run-ship-cli.ts +131 -39
  582. package/scripts/run/ship/run-ship-conflict-probe.ts +59 -0
  583. package/scripts/run/ship/run-ship-detach.ts +10 -4
  584. package/scripts/run/ship/run-ship-handover.ts +106 -0
  585. package/scripts/run/ship/run-ship-infra-retry.ts +16 -1
  586. package/scripts/run/ship/run-ship-lap.ts +63 -0
  587. package/scripts/run/ship/run-ship-next.ts +27 -26
  588. package/scripts/run/ship/run-ship-overlap.ts +8 -3
  589. package/scripts/run/ship/run-ship-place.ts +9 -2
  590. package/scripts/run/ship/run-ship-planned.ts +92 -0
  591. package/scripts/run/ship/run-ship-pre-detach-pinned.ts +197 -0
  592. package/scripts/run/ship/run-ship-pre-detach-timed.ts +7 -8
  593. package/scripts/run/ship/run-ship-pre-detach.ts +77 -23
  594. package/scripts/run/ship/run-ship-preflight.ts +16 -4
  595. package/scripts/run/ship/run-ship-probe.ts +23 -9
  596. package/scripts/run/ship/run-ship-relaunch-cap.ts +43 -7
  597. package/scripts/run/ship/run-ship-relaunch.ts +51 -0
  598. package/scripts/run/ship/run-ship-return.ts +103 -29
  599. package/scripts/run/ship/run-ship-review-reason.ts +24 -0
  600. package/scripts/run/ship/run-ship-review-steps.ts +230 -59
  601. package/scripts/run/ship/run-ship-review.ts +171 -51
  602. package/scripts/run/ship/run-ship-reviewer.ts +138 -42
  603. package/scripts/run/ship/run-ship-scoped.ts +39 -10
  604. package/scripts/run/ship/run-ship-stage.ts +49 -13
  605. package/scripts/run/ship/run-ship-steps.ts +24 -4
  606. package/scripts/run/ship/run-ship-stop-cause.ts +43 -2
  607. package/scripts/run/ship/run-ship-stop-context.ts +50 -13
  608. package/scripts/run/ship/run-ship-sync.ts +171 -30
  609. package/scripts/run/ship/run-ship-synced-scope.ts +178 -0
  610. package/scripts/run/ship/run-ship-wait-cli.ts +145 -0
  611. package/scripts/run/ship/run-ship-waiter.ts +114 -0
  612. package/scripts/run/ship/run-ship.ts +16 -2
  613. package/scripts/run/wake/run-wake-caffeinate.ts +48 -0
  614. package/scripts/run/wake/run-wake-cli.ts +6 -113
  615. package/scripts/run/wake/run-wake-code.ts +53 -11
  616. package/scripts/run/wake/run-wake-driver.ts +5 -3
  617. package/scripts/run/wake/run-wake-finish.ts +137 -0
  618. package/scripts/run/wake/run-wake-loop.ts +5 -3
  619. package/scripts/run/wake/run-wake-ports.ts +11 -3
  620. package/scripts/run/wake/run-wake-session.ts +2 -0
  621. package/scripts/run/wake/run-wake.ts +3 -2
  622. package/scripts/safe-chain/ci-installer-pin.ts +49 -8
  623. package/scripts/safe-chain/project-config.ts +3 -2
  624. package/scripts/security/secretlint-scan.ts +42 -9
  625. package/scripts/security/security-audit-provision-main.ts +9 -1
  626. package/scripts/security/security-audit-provision.ts +1 -1
  627. package/scripts/security/security-audit.ts +21 -1
  628. package/scripts/security/sonar-file.ts +10 -5
  629. package/scripts/split/split-assess.ts +12 -3
  630. package/scripts/split/split-early-notice.ts +116 -0
  631. package/scripts/split/split-measure.ts +20 -2
  632. package/scripts/split/split-resolution.ts +107 -0
  633. package/scripts/test/direct-importers.ts +185 -0
  634. package/scripts/test/pure-files-entries.ts +83 -0
  635. package/scripts/test/related-heavy-tests.ts +81 -0
  636. package/scripts/test/test-no-base.ts +88 -0
  637. package/scripts/test/test-pinned-cache-logic.ts +363 -0
  638. package/scripts/test/test-pinned-cache.ts +337 -0
  639. package/scripts/test/test-pinned-emit.ts +49 -0
  640. package/scripts/test/test-pinned-functions.ts +116 -0
  641. package/scripts/test/test-pinned-ledger.ts +80 -0
  642. package/scripts/test/test-pinned-logic.ts +208 -20
  643. package/scripts/test/test-pinned-main.ts +84 -18
  644. package/scripts/test/test-pinned-pool.ts +73 -0
  645. package/scripts/test/test-pinned-probe.ts +254 -0
  646. package/scripts/test/test-pinned-prune-logic.ts +118 -0
  647. package/scripts/test/test-pinned-reach.ts +194 -0
  648. package/scripts/test/test-pinned-record.ts +196 -0
  649. package/scripts/test/test-pinned-rewire.ts +148 -0
  650. package/scripts/test/test-pinned-spawn.ts +81 -0
  651. package/scripts/test/test-pinned-stage.ts +70 -0
  652. package/scripts/test/test-pinned.ts +258 -47
  653. package/scripts/test/test-reach-cli.ts +61 -0
  654. package/scripts/test/test-read-map.ts +35 -11
  655. package/scripts/test/test-read-recording.ts +17 -5
  656. package/scripts/test/test-read-trace.ts +26 -8
  657. package/scripts/test/test-red-logic.ts +46 -7
  658. package/scripts/test/test-red.ts +34 -14
  659. package/scripts/test/test-related-claim.ts +37 -0
  660. package/scripts/test/test-related-main.ts +4 -1
  661. package/scripts/test/test-related-moved.ts +91 -0
  662. package/scripts/test/test-related-record.ts +43 -23
  663. package/scripts/test/test-related-scope.ts +99 -34
  664. package/scripts/test/test-related-synced.ts +135 -0
  665. package/scripts/test/test-related-touched.ts +84 -0
  666. package/scripts/test/test-related.ts +167 -83
  667. package/scripts/test/test-unit-guard.ts +28 -5
  668. package/scripts/test/unit-durations.ts +41 -0
  669. package/scripts/test/unit-worker-share.ts +5 -4
  670. package/scripts/test/vitest-cache-directory.ts +19 -0
  671. package/scripts/test/vitest-cli-excludes.ts +14 -0
  672. package/scripts/test/vitest-failure-summary.ts +111 -0
  673. package/scripts/test/whole-suite-files.ts +12 -1
  674. package/scripts/time-runtime/time-batch-guard.ts +42 -4
  675. package/scripts/time-runtime/time-bundle-call.ts +1 -1
  676. package/scripts/time-runtime/time-bundles.ts +36 -5
  677. package/scripts/time-runtime/time-markers.ts +2 -2
  678. package/scripts/time-runtime/time-search-call.ts +38 -0
  679. package/scripts/time-runtime/time-shell.ts +1 -1
  680. package/scripts/time-runtime/time-transcript-line.ts +17 -5
  681. package/scripts/version/development-engines-verdict.ts +124 -0
  682. package/scripts/version/latest-corepack.ts +2 -0
  683. package/templates/gitignore +2 -0
  684. package/templates/workflows/ci.yml +41 -0
  685. package/dist/hooks/chunk-AZX4FGQ3.js +0 -207
  686. package/dist/hooks/chunk-PT5R6UJD.js +0 -7131
  687. package/dist/hooks/chunk-TUIFS5E7.js +0 -72
  688. package/dist/hooks/chunk-YCDOIR3I.js +0 -3456
  689. package/scripts/cost-runtime/context-cut-threshold.ts +0 -11
  690. package/scripts/rules/filing-cap.ts +0 -139
@@ -0,0 +1,124 @@
1
+ import { git_command } from '#scripts/git/git-command'
2
+ import { session_cite } from '#scripts/issue/session-cite'
3
+ import { error_text } from '#scripts/lib/error-message'
4
+ import { run_board_closed, type ClosedIssue } from '#scripts/run/board/run-board-closed'
5
+ import { run_event_scope } from '#scripts/run/event/run-event-scope'
6
+ import type { RunEvent } from '#scripts/run/event/run-event-stream'
7
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
8
+ import { lane_registry, type LaneInfo } from './lane-registry'
9
+ import { lane_withdraw, type Withdrawer } from './lane-withdraw'
10
+
11
+ // Every open lane whose issue was closed on GitHub as not planned is withdrawn through `lane:withdraw`'s
12
+ // own path (joshuafolkken/kit#4915): its child is stopped, its work stashed and the lane closed, so the
13
+ // lane leaves the count and no cut resumes it. Nothing else ends such a lane: the child keeps working on
14
+ // an issue no one wants, and the lane limit counts it while the board draws no lane for it.
15
+ //
16
+ // **Only a not-planned close is swept.** A merged issue's lane is ended by the merge path, where a
17
+ // detached ship may still be running its tail; an issue closed as completed by hand is left to
18
+ // `lane:prune` and `run:merge`, which books it withdrawn when no merged pull request closed it
19
+ // (joshuafolkken/kit#4916). An issue that could not be read is
20
+ // asked again on the next pass, and a lane whose withdrawal is already on the event stream is not
21
+ // withdrawn twice.
22
+ //
23
+ // **A branch with commits on no remote is kept**: the close deletes the branch and the stash keeps only
24
+ // uncommitted work, so a person decides with `lane:withdraw` by hand. `run:merge` withdraws through the
25
+ // same guard (`withdraw_lane`).
26
+
27
+ const SWEEP_REASON = 'its issue was closed as not planned while the lane was still open'
28
+
29
+ function is_not_planned(read: ClosedIssue | undefined): boolean {
30
+ return read !== undefined && read.is_not_planned && !read.is_merged
31
+ }
32
+
33
+ /** The open lanes whose issue is closed as not planned. */
34
+ async function closed_lanes(): Promise<Array<LaneInfo>> {
35
+ const listed = await lane_registry.list_lanes()
36
+ const lanes = listed.filter((lane) => !lane.is_stranded)
37
+ const { closed } = await run_board_closed.read_all(lanes.map((lane) => Number(lane.issue)))
38
+
39
+ return lanes.filter((lane) => is_not_planned(closed.get(Number(lane.issue))))
40
+ }
41
+
42
+ // An unreadable count keeps the branch, as unpushed commits would.
43
+ async function has_unpushed(branch: string): Promise<boolean> {
44
+ try {
45
+ return (await git_command.commit_count_unpushed(branch)) > 0
46
+ } catch (error) {
47
+ error_text.trace_swallowed('lane_withdraw_closed.has_unpushed', error)
48
+
49
+ return true
50
+ }
51
+ }
52
+
53
+ function kept_note(issue: string, by: Withdrawer): string {
54
+ const settled = by === 'yield' ? 'gave its lane back' : 'is closed without a merge'
55
+
56
+ return `${session_cite.issue(issue)} ${settled}, but its branch has commits on no remote, so its lane is kept — run \`pnpm josh lane:withdraw ${issue}\` by hand.`
57
+ }
58
+
59
+ interface UnattendedWithdrawal {
60
+ note: string
61
+ // The withdrawal could not read the issue, so nothing was stopped or closed.
62
+ is_retry: boolean
63
+ }
64
+
65
+ /** Withdraw a closed issue's open lane with no person present, keeping a branch with unpushed commits. */
66
+ async function withdraw_lane(
67
+ lane: LaneInfo,
68
+ reason: string,
69
+ by: Withdrawer,
70
+ ): Promise<UnattendedWithdrawal> {
71
+ if (await has_unpushed(lane.branch)) return { note: kept_note(lane.issue, by), is_retry: false }
72
+ const outcome = await lane_withdraw.withdraw(lane.issue, reason, by)
73
+
74
+ return {
75
+ note: lane_withdraw.describe(outcome, lane.issue),
76
+ is_retry: outcome.kind === 'unreadable',
77
+ }
78
+ }
79
+
80
+ async function sweep_lane(
81
+ lane: LaneInfo,
82
+ events: ReadonlyArray<RunEvent>,
83
+ ): Promise<string | undefined> {
84
+ if (run_event_scope.is_withdrawn(events, lane.issue)) return undefined
85
+ const { note } = await withdraw_lane(lane, SWEEP_REASON, 'supervisor')
86
+
87
+ return note
88
+ }
89
+
90
+ // One lane's failure does not stop the sweep: the rest are still withdrawn, and this one is asked again.
91
+ async function try_sweep_lane(
92
+ lane: LaneInfo,
93
+ events: ReadonlyArray<RunEvent>,
94
+ ): Promise<string | undefined> {
95
+ try {
96
+ return await sweep_lane(lane, events)
97
+ } catch (error) {
98
+ error_text.trace_swallowed('lane_withdraw_closed.sweep_lane', error)
99
+
100
+ return `${session_cite.issue(lane.issue)} could not be withdrawn (${error_text.message_of(error)}); the next pass tries again.`
101
+ }
102
+ }
103
+
104
+ /** Withdraw every open lane whose issue is closed as not planned — the supervisor's pass; one note each. */
105
+ async function withdraw_closed(): Promise<Array<string>> {
106
+ const lanes = await closed_lanes()
107
+ const events = await run_event_stream_emit.current_events()
108
+ const notes: Array<string> = []
109
+
110
+ // Sequential, as `lane_close.close_each` is: two worktree removals at once contend for git's
111
+ // registration directory.
112
+ for (const lane of lanes) {
113
+ // eslint-disable-next-line no-await-in-loop -- worktree writes contend for the same registration directory
114
+ const note = await try_sweep_lane(lane, events)
115
+
116
+ if (note !== undefined) notes.push(note)
117
+ }
118
+
119
+ return notes
120
+ }
121
+
122
+ const lane_withdraw_closed = { SWEEP_REASON, withdraw_closed, withdraw_lane }
123
+
124
+ export { lane_withdraw_closed }
@@ -12,6 +12,7 @@ import { lane_reap } from './lane-reap'
12
12
  import { lane_registry, type LaneInfo } from './lane-registry'
13
13
  import { lane_residents } from './lane-residents'
14
14
  import { lane_state } from './lane-state'
15
+ import { lane_yield } from './lane-yield'
15
16
 
16
17
  // `josh lane:withdraw <N> --reason <text>` — a person takes a running lane child's issue out of the
17
18
  // current run: the child is stopped, its work stashed and the lane closed. **The Issue stays open** —
@@ -25,9 +26,15 @@ import { lane_state } from './lane-state'
25
26
  //
26
27
  // A tree that could not be stashed is left in place: closing the lane would delete the work, and the
27
28
  // person decides what to keep.
29
+ //
30
+ // **An issue closed on GitHub while its lane stayed open is withdrawn the same way**
31
+ // (joshuafolkken/kit#4915): its child would otherwise keep working, be resumed by the next cut and
32
+ // hold a lane. `lane-withdraw-closed.ts` sweeps those lanes each supervisor pass, and the event, the
33
+ // stash note and the issue comment name the supervisor rather than a person. `run:merge` withdraws a
34
+ // returned child whose issue closed without a merge the same way, named as itself
35
+ // (`run-merge-withdrawn.ts`, joshuafolkken/kit#4916).
28
36
 
29
37
  const WITHDRAW_OCCASION = 'lane withdraw'
30
- const WITHDRAW_CAUSE = 'a person withdrew it with `lane:withdraw`'
31
38
  const CLOSED_STATE = 'CLOSED'
32
39
  const CLOSED_KIND = 'closed'
33
40
  const NO_LANE_KIND = 'no-lane'
@@ -35,13 +42,63 @@ const UNREADABLE_KIND = 'unreadable'
35
42
 
36
43
  type Unsaved = Extract<KeptWork, { kind: 'unsaved' }>
37
44
 
45
+ // Who withdrew the lane: a person with `lane:withdraw`, the supervisor's sweep of closed issues,
46
+ // `run:merge` collecting a child whose issue closed without a merge (joshuafolkken/kit#4916), or the
47
+ // child's own `lane:plan` giving its lane back (`lane-yield.ts`, joshuafolkken/kit#4926).
48
+ type Withdrawer = 'person' | 'supervisor' | 'merge' | 'yield'
49
+
50
+ const OPEN_TAIL =
51
+ 'Its lane child was stopped and the lane closed. The Issue stays open: this run does not resume it, and the next run offers it again.'
52
+ // What `describe` says of an open issue's place in the run once its lane is withdrawn.
53
+ const STAYS_OPEN = 'The Issue stays open: this run does not offer it again, the next run does.'
54
+
55
+ // How each withdrawer is named on the event stream, in the stash note and on the issue.
56
+ const WITHDRAWER_TEXT = {
57
+ person: {
58
+ name: 'a person',
59
+ cause: 'a person withdrew it with `lane:withdraw`',
60
+ closed_head:
61
+ 'Its lane was withdrawn with `pnpm josh lane:withdraw` after the Issue was closed.',
62
+ open_head: 'Withdrawn from the current run by a person with `pnpm josh lane:withdraw`.',
63
+ open_tail: OPEN_TAIL,
64
+ stays: STAYS_OPEN,
65
+ },
66
+ supervisor: {
67
+ name: 'the run:wake supervisor',
68
+ cause: 'the `run:wake` supervisor withdrew it after its issue was closed',
69
+ closed_head: 'Its lane was withdrawn by the `run:wake` supervisor after the Issue was closed.',
70
+ open_head: 'Withdrawn from the current run by the `run:wake` supervisor.',
71
+ open_tail: OPEN_TAIL,
72
+ stays: STAYS_OPEN,
73
+ },
74
+ merge: {
75
+ name: 'the run:merge driver',
76
+ cause: '`run:merge` withdrew it after its issue was closed without a merge',
77
+ closed_head:
78
+ 'Its lane was withdrawn by `pnpm josh run:merge` after the Issue was closed without a merge.',
79
+ open_head: 'Withdrawn from the current run by `pnpm josh run:merge`.',
80
+ open_tail: OPEN_TAIL,
81
+ stays: STAYS_OPEN,
82
+ },
83
+ yield: lane_yield.YIELD_TEXT,
84
+ } as const satisfies Record<Withdrawer, Record<string, string>>
85
+
86
+ interface Withdrawal {
87
+ reason: string
88
+ by: Withdrawer
89
+ is_closed: boolean
90
+ }
91
+
38
92
  interface Withdrawn {
39
93
  kind: 'withdrawn'
94
+ by: Withdrawer
40
95
  // How many of the child's processes the withdrawal stopped — 0 for a child that had already ended.
41
96
  reaped: number
42
97
  stash: string | undefined
43
- // Whether the comment saying the issue stays open reached it.
98
+ // Whether the comment saying so reached the issue.
44
99
  is_noted: boolean
100
+ // The issue was already closed — withdrawn or finished on GitHub while its lane stayed open.
101
+ is_closed: boolean
45
102
  lane: CloseKind
46
103
  // What else the close ended from the checkout; `undefined` where it could not be read.
47
104
  residents: ReadonlyArray<number> | undefined
@@ -51,51 +108,54 @@ type Refused =
51
108
  { kind: typeof CLOSED_KIND } | { kind: typeof NO_LANE_KIND } | { kind: typeof UNREADABLE_KIND }
52
109
  type WithdrawOutcome = Withdrawn | Unsaved | Refused
53
110
 
54
- function withdraw_comment(reason: string): string {
55
- return [
56
- 'Withdrawn from the current run by a person with `pnpm josh lane:withdraw`.',
57
- 'Its lane child was stopped and the lane closed. The Issue stays open: this run does not resume it, and the next run offers it again.',
58
- `- Why: ${reason}`,
59
- ].join('\n')
111
+ function withdraw_comment({ reason, by, is_closed }: Withdrawal): string {
112
+ const head = is_closed ? WITHDRAWER_TEXT[by].closed_head : WITHDRAWER_TEXT[by].open_head
113
+ const tail = is_closed
114
+ ? 'Its lane child was stopped and the lane closed, so it no longer holds a lane and is not resumed.'
115
+ : WITHDRAWER_TEXT[by].open_tail
116
+
117
+ return [head, tail, `- Why: ${reason}`].join('\n')
60
118
  }
61
119
 
62
- async function record_withdraw(issue: string, reason: string): Promise<void> {
120
+ async function record_withdraw(issue: string, { reason, by }: Withdrawal): Promise<void> {
63
121
  await run_event_stream_emit.emit(
64
122
  run_event_stream.EVENT_KIND.WITHDRAW,
65
- `${issue_cite.plain(issue)} withdrawn by a person: ${reason}`,
123
+ `${issue_cite.plain(issue)} withdrawn by ${WITHDRAWER_TEXT[by].name}: ${reason}`,
66
124
  )
67
125
  }
68
126
 
69
- // A closed issue has nothing to withdraw, and an unread one might be: neither writes anything.
70
- async function refusal_of(issue: string): Promise<Refused | undefined> {
127
+ // Whether the issue is closed, or `undefined` when it could not be read — an unread issue writes nothing.
128
+ async function read_is_closed(issue: string): Promise<boolean | undefined> {
71
129
  const read = await issue_state_cli.read_issue(issue)
72
130
 
73
- if (read.kind !== 'state') return { kind: UNREADABLE_KIND }
74
- if (read.state.state.toUpperCase() === CLOSED_STATE) return { kind: CLOSED_KIND }
131
+ if (read.kind !== 'state') return undefined
75
132
 
76
- return undefined
133
+ return read.state.state.toUpperCase() === CLOSED_STATE
77
134
  }
78
135
 
79
136
  async function note_issue(
80
137
  issue: string,
81
- reason: string,
138
+ withdrawal: Withdrawal,
82
139
  stash: string | undefined,
83
140
  ): Promise<boolean> {
84
- const is_noted = await git_gh_issue_write.issue_try_comment(issue, withdraw_comment(reason))
141
+ const is_noted = await git_gh_issue_write.issue_try_comment(issue, withdraw_comment(withdrawal))
142
+
143
+ if (stash !== undefined) {
144
+ await lane_preserve.announce(issue, stash, WITHDRAWER_TEXT[withdrawal.by].cause)
145
+ }
85
146
 
86
- if (stash !== undefined) await lane_preserve.announce(issue, stash, WITHDRAW_CAUSE)
87
147
  await run_label.unmark(issue)
88
148
 
89
149
  return is_noted
90
150
  }
91
151
 
92
- async function stop_and_close(lane: LaneInfo, reason: string): Promise<WithdrawOutcome> {
93
- await record_withdraw(lane.issue, reason)
152
+ async function stop_and_close(lane: LaneInfo, withdrawal: Withdrawal): Promise<WithdrawOutcome> {
153
+ await record_withdraw(lane.issue, withdrawal)
94
154
  const reaped = lane_reap.reap_child(lane).length
95
155
  const saved = await lane_preserve.keep_work(lane.issue, lane.directory, WITHDRAW_OCCASION)
96
156
 
97
157
  if (saved.kind === 'unsaved') return saved
98
- const is_noted = await note_issue(lane.issue, reason, saved.stash)
158
+ const is_noted = await note_issue(lane.issue, withdrawal, saved.stash)
99
159
  const closed = await lane_close.close_lane(lane.issue)
100
160
 
101
161
  // After the close, so the record names the withdrawal rather than the close it ran. An `incomplete`
@@ -106,25 +166,34 @@ async function stop_and_close(lane: LaneInfo, reason: string): Promise<WithdrawO
106
166
 
107
167
  return {
108
168
  kind: 'withdrawn',
169
+ by: withdrawal.by,
109
170
  reaped,
110
171
  stash: saved.stash,
111
172
  is_noted,
173
+ is_closed: withdrawal.is_closed,
112
174
  lane: closed.kind,
113
175
  residents: closed.residents,
114
176
  }
115
177
  }
116
178
 
117
- /** Stop `#<N>`'s lane child, stash its work and close the lane, leaving the Issue open. */
118
- async function withdraw(issue: string, reason: string): Promise<WithdrawOutcome> {
179
+ /**
180
+ * Stop `#<N>`'s lane child, stash its work and close the lane, leaving the Issue as it is. A closed
181
+ * issue is refused only when no lane is left for it (joshuafolkken/kit#4915).
182
+ */
183
+ async function withdraw(
184
+ issue: string,
185
+ reason: string,
186
+ by: Withdrawer = 'person',
187
+ ): Promise<WithdrawOutcome> {
119
188
  issue_number_shape.require_issue_number(issue)
120
- const refusal = await refusal_of(issue)
189
+ const is_closed = await read_is_closed(issue)
121
190
 
122
- if (refusal !== undefined) return refusal
191
+ if (is_closed === undefined) return { kind: UNREADABLE_KIND }
123
192
  const lane = await lane_registry.find_open_lane(issue)
124
193
 
125
- if (lane === undefined) return { kind: NO_LANE_KIND }
194
+ if (lane === undefined) return { kind: is_closed ? CLOSED_KIND : NO_LANE_KIND }
126
195
 
127
- return await stop_and_close(lane, reason)
196
+ return await stop_and_close(lane, { reason, by, is_closed })
128
197
  }
129
198
 
130
199
  function stash_sentence(stash: string | undefined): string {
@@ -147,8 +216,11 @@ function withdrawn_text(outcome: Withdrawn, issue: string): string {
147
216
  const head = `Withdrew ${cited}. ${stopped} ${stash_sentence(outcome.stash)}`
148
217
  const lane = lane_suffix(outcome, cited)
149
218
  const noted = outcome.is_noted ? '' : ' The comment saying so could not be posted.'
219
+ const issue_sentence = outcome.is_closed
220
+ ? 'The Issue was already closed, so its lane no longer holds a place.'
221
+ : WITHDRAWER_TEXT[outcome.by].stays
150
222
 
151
- return `${head} The Issue stays open: this run does not offer it again, the next run does.${noted}${lane}`
223
+ return `${head} ${issue_sentence}${noted}${lane}`
152
224
  }
153
225
 
154
226
  function unsaved_text(outcome: Unsaved, issue: string): string {
@@ -159,7 +231,7 @@ function refused_text(outcome: Refused, issue: string): string {
159
231
  const cited = session_cite.issue(issue)
160
232
 
161
233
  if (outcome.kind === CLOSED_KIND) {
162
- return `${cited} is already closed, so there is nothing to withdraw.`
234
+ return `${cited} is already closed and no lane is open for it, so there is nothing to withdraw.`
163
235
  }
164
236
 
165
237
  if (outcome.kind === NO_LANE_KIND) {
@@ -177,7 +249,12 @@ function describe(outcome: WithdrawOutcome, issue: string): string {
177
249
  : refused_text(outcome, issue)
178
250
  }
179
251
 
180
- const lane_withdraw = { describe, withdraw }
252
+ // How every withdrawer is named in the `withdraw` event — the board reads the reason after it.
253
+ const WITHDRAWER_NAMES: ReadonlyArray<string> = Object.values(WITHDRAWER_TEXT).map(
254
+ (text) => text.name,
255
+ )
256
+
257
+ const lane_withdraw = { WITHDRAWER_NAMES, describe, withdraw }
181
258
 
182
- export type { WithdrawOutcome }
259
+ export type { Withdrawer, WithdrawOutcome }
183
260
  export { lane_withdraw }
@@ -0,0 +1,85 @@
1
+ import { issue_cite } from '#scripts/issue/issue-cite'
2
+ import { run_event_scope } from '#scripts/run/event/run-event-scope'
3
+ import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
4
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
5
+
6
+ // A lane child that gives its lane back (joshuafolkken/kit#4926): `lane:plan` found a running lane that
7
+ // planned one of its files first, so instead of polling for the holder to end while its seat stays taken,
8
+ // it notes its yield on the stream and ends. `run:merge` books it `yielded` — neither a merge, a failure
9
+ // nor an outage — and closes the lane through `lane:withdraw`'s path, named as this withdrawer.
10
+ //
11
+ // **The yield is a `note`, not a `withdraw`**: a `withdraw` event says the lane is already closed, and
12
+ // the readers of the stream (`backlog-drive-restore.ts`, the `run:wake` sweep, `run:tidy`) act on that.
13
+ // The yielding child cannot close its own lane, so its issue stays in flight until `run:merge` does.
14
+ //
15
+ // **Unlike a person's withdrawal, a yield does not take the issue out of the run.** Its plan outlives the
16
+ // lane (`lane-plan.ts` → `release`), so `backlog:next` holds it back on that plan while the holder runs
17
+ // and offers it again once the holder's lane ends; `backlog-drive-restore.ts` reads `is_yield` to leave
18
+ // the closing withdrawal out of the withdrawn and the settled issues.
19
+
20
+ // The reason `run:merge` closes a yielded lane with.
21
+ const YIELD_REASON = 'it gave its lane back to an earlier lane holding one of its planned files'
22
+
23
+ const YIELD_MARK = ' yielded its lane: '
24
+
25
+ const YIELD_TEXT = {
26
+ name: 'its own lane:plan',
27
+ cause: '`lane:plan` gave its lane back to an earlier lane holding one of its planned files',
28
+ closed_head: 'Its lane was given back by `pnpm josh lane:plan` after the Issue was closed.',
29
+ open_head:
30
+ 'Gave its lane back: `pnpm josh lane:plan` found an earlier lane holding one of its planned files.',
31
+ open_tail:
32
+ 'The lane closed before any implementation edit. The Issue stays in the run: it is offered again once the holding lane ends, keeping its place in line.',
33
+ stays: 'The Issue stays in the run: it is offered again once the holding lane ends.',
34
+ } as const
35
+
36
+ // Where in the lane the yield fell, one word on the note, kept for a later count of how many the entry
37
+ // probe caught before the investigation (joshuafolkken/kit#5211): `entry` from `run:entry`, `summary` from the
38
+ // `lane:plan` a child runs once its work summary is set.
39
+ type YieldAt = 'entry' | 'summary'
40
+
41
+ const YIELD_AT_MARK = ' · at: '
42
+
43
+ async function record_yield(issue: number, reason: string, at: YieldAt): Promise<void> {
44
+ await run_event_stream_emit.emit(
45
+ run_event_stream.EVENT_KIND.NOTE,
46
+ `${issue_cite.plain(String(issue))}${YIELD_MARK}${reason}${YIELD_AT_MARK}${at}`,
47
+ )
48
+ }
49
+
50
+ function is_yield_note(event: RunEvent): boolean {
51
+ return event.kind === run_event_stream.EVENT_KIND.NOTE && event.text.includes(YIELD_MARK)
52
+ }
53
+
54
+ // Whether `event` is the withdrawal `run:merge` closed a yielded lane with, rather than one that takes
55
+ // the issue out of the run.
56
+ function is_yield(event: RunEvent): boolean {
57
+ return (
58
+ event.kind === run_event_stream.EVENT_KIND.WITHDRAW &&
59
+ event.text.includes(` withdrawn by ${YIELD_TEXT.name}:`)
60
+ )
61
+ }
62
+
63
+ // An event that decides whether the issue's newest launch yielded: the launch, the yield, or a
64
+ // withdrawal that is not the yield's own close.
65
+ function is_deciding(event: RunEvent, issue: string): boolean {
66
+ if (run_event_scope.issue_named(event) !== issue) return false
67
+ if (event.kind === run_event_stream.EVENT_KIND.CHILD_LAUNCH) return true
68
+
69
+ return (
70
+ is_yield_note(event) ||
71
+ (event.kind === run_event_stream.EVENT_KIND.WITHDRAW && !is_yield(event))
72
+ )
73
+ }
74
+
75
+ // Whether the issue's newest launch gave its lane back, and no one withdrew it since.
76
+ function is_yielded(events: ReadonlyArray<RunEvent>, issue: string): boolean {
77
+ const newest = events.findLast((event) => is_deciding(event, issue))
78
+
79
+ return newest !== undefined && is_yield_note(newest)
80
+ }
81
+
82
+ const lane_yield = { YIELD_REASON, YIELD_TEXT, is_yield, is_yielded, record_yield }
83
+
84
+ export { lane_yield }
85
+ export type { YieldAt }
@@ -1,4 +1,4 @@
1
- import { setTimeout as sleep } from 'node:timers/promises'
1
+ import { poll } from '#scripts/lib/poll'
2
2
  import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
3
3
  import { run_ship_probe } from '#scripts/run/ship/run-ship-probe'
4
4
 
@@ -12,19 +12,20 @@ async function current(issue: string): Promise<ShipReading | undefined> {
12
12
  return repository === undefined ? undefined : run_ship_detach.read_result(repository, issue)
13
13
  }
14
14
 
15
+ // The ship's ending once it leaves `running`, and `abnormal` once its result is gone.
16
+ async function settled(issue: string): Promise<Exclude<ShipEnding, 'none'> | undefined> {
17
+ const read = await current(issue)
18
+ if (read === undefined) return 'abnormal'
19
+
20
+ return read.result === 'running' ? undefined : read.result
21
+ }
22
+
15
23
  async function wait_for_ship(issue: string, prior_launch: string | undefined): Promise<ShipEnding> {
16
- let read = await current(issue)
24
+ const read = await current(issue)
17
25
  if (read === undefined || read.launch_id === prior_launch) return 'none'
26
+ if (read.result !== 'running') return read.result
18
27
 
19
- while (read.result === 'running') {
20
- // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
21
- await sleep(POLL_MS)
22
- // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
23
- read = await current(issue)
24
- if (read === undefined) return 'abnormal'
25
- }
26
-
27
- return read.result
28
+ return await poll.wait_for(async () => await settled(issue), { interval_ms: POLL_MS })
28
29
  }
29
30
 
30
31
  const openai_lane_ship = { current, wait_for_ship }
@@ -1,5 +1,6 @@
1
1
  import { stamp_file } from '#scripts/josh/stamp-file'
2
2
  import { json_value } from '#scripts/lib/json-value'
3
+ import { poll } from '#scripts/lib/poll'
3
4
  import { z } from 'zod'
4
5
 
5
6
  const DECISION_PREFIX = 'josh-openai-lane-supervisor-decision-'
@@ -40,15 +41,12 @@ function cancel(lane_directory: string, nonce: string): boolean {
40
41
  }
41
42
 
42
43
  async function is_approved(lane_directory: string, nonce: string): Promise<boolean> {
43
- for (let attempt = 0; attempt < APPROVAL_POLL_LIMIT; attempt += 1) {
44
- const decision = read(lane_directory, nonce)?.decision
45
- if (decision !== undefined) return decision === 'approved'
44
+ const decision = await poll.poll_for(() => read(lane_directory, nonce)?.decision, {
45
+ attempts: APPROVAL_POLL_LIMIT,
46
+ interval_ms: APPROVAL_POLL_MS,
47
+ })
46
48
 
47
- // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
48
- await new Promise((resolve) => setTimeout(resolve, APPROVAL_POLL_MS))
49
- }
50
-
51
- return false
49
+ return decision === 'approved'
52
50
  }
53
51
 
54
52
  function remove(lane_directory: string, nonce: string): void {
@@ -4,6 +4,7 @@ import { process_identity } from '#scripts/josh/process-identity'
4
4
  import { process_owner_schema } from '#scripts/josh/process-owner'
5
5
  import { stamp_file } from '#scripts/josh/stamp-file'
6
6
  import { json_value } from '#scripts/lib/json-value'
7
+ import { poll } from '#scripts/lib/poll'
7
8
  import { z } from 'zod'
8
9
  import { openai_lane_supervisor_decision } from './openai-lane-supervisor-decision'
9
10
 
@@ -66,14 +67,10 @@ function active(lane_directory: string): SupervisorOwner | undefined {
66
67
  }
67
68
 
68
69
  async function wait_for_active(lane_directory: string): Promise<SupervisorOwner | undefined> {
69
- for (let attempt = 0; attempt < CLAIM_POLL_LIMIT; attempt += 1) {
70
- const owner = active(lane_directory)
71
- if (owner !== undefined) return owner
72
- // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
73
- await new Promise((resolve) => setTimeout(resolve, CLAIM_POLL_MS))
74
- }
75
-
76
- return undefined
70
+ return await poll.poll_for(() => active(lane_directory), {
71
+ attempts: CLAIM_POLL_LIMIT,
72
+ interval_ms: CLAIM_POLL_MS,
73
+ })
77
74
  }
78
75
 
79
76
  // The hard link snapshots the exact owner inode. Several contenders may snapshot one stale owner,
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url'
3
3
  import { agent_argv } from '#scripts/agent/agent-argv'
4
4
  import type { AgentProfile } from '#scripts/agent/agent-role-profile'
5
5
  import { process_identity } from '#scripts/josh/process-identity'
6
+ import { poll } from '#scripts/lib/poll'
6
7
  import { run_cut, type RunCut } from '#scripts/run/cut/run-cut'
7
8
  import { detached_launch, type LaunchArgv } from '#scripts/run/detached-launch'
8
9
  import { lane_child_invocation } from './lane-child-invocation'
@@ -142,10 +143,10 @@ async function wait_for_inherited_child(
142
143
  child_pid: number,
143
144
  child_process_start: string | undefined,
144
145
  ): Promise<void> {
145
- while (process_identity.is_same_process(child_pid, child_process_start) !== false) {
146
- // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
147
- await new Promise((resolve) => setTimeout(resolve, CHILD_POLL_MS))
148
- }
146
+ await poll.wait_for(
147
+ () => process_identity.is_same_process(child_pid, child_process_start) === false || undefined,
148
+ { interval_ms: CHILD_POLL_MS },
149
+ )
149
150
  }
150
151
 
151
152
  async function recovered_resume(lane: LaneInfo, nonce: string): Promise<boolean | undefined> {
@@ -1,4 +1,4 @@
1
- // One bounded polling loop and one sleep, in one place.
1
+ // One polling loop — bounded by attempts, or ended only by its read — and one sleep, in one place.
2
2
  //
3
3
  // **Three modules had already written the same three-line `sleep`** — `gh/git-pr-checks.ts`,
4
4
  // `eval/eval-run.ts` and `propagate/propagate-publish.ts` — and the release command's tag watch
@@ -24,9 +24,6 @@ interface PollOptions {
24
24
  sleeper?: (duration_ms: number) => Promise<void>
25
25
  }
26
26
 
27
- // **True as soon as `is_done` answers true, false when the attempts run out.** The first attempt runs
28
- // before any wait, so a condition already met costs nothing — which is the common case for a tag that
29
- // was created while the command was doing something else.
30
27
  // **The wait goes between attempts, never after the last one**, so the budget a caller computes from
31
28
  // `attempts × interval_ms` is the budget it actually spends. The pre-existing merge-gate loop in
32
29
  // `git-pr-checks.ts` guards its sleep the same way; extracted here so the loop keeps one branch.
@@ -39,21 +36,53 @@ async function wait_between(
39
36
  if (attempt < attempts - 1) await wait(interval_ms)
40
37
  }
41
38
 
42
- async function poll_until(is_done: () => Promise<boolean>, options: PollOptions): Promise<boolean> {
39
+ type WaitOptions = Omit<PollOptions, 'attempts'>
40
+
41
+ // **The first value `read` answers, `undefined` when the attempts run out.** `undefined` is "not yet",
42
+ // so a read that reaches a verdict — success or failure — answers it and the loop ends there.
43
+ async function poll_for<T>(
44
+ read: () => Promise<T | undefined> | T | undefined,
45
+ options: PollOptions,
46
+ ): Promise<T | undefined> {
43
47
  const wait = options.sleeper ?? sleep
44
48
 
45
49
  for (let attempt = 0; attempt < options.attempts; attempt += 1) {
46
50
  // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
47
- if (await is_done()) return true
51
+ const value = await read()
52
+ if (value !== undefined) return value
48
53
 
49
54
  // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
50
55
  await wait_between(wait, options.interval_ms, attempt, options.attempts)
51
56
  }
52
57
 
53
- return false
58
+ return undefined
59
+ }
60
+
61
+ // **True as soon as `is_done` answers true, false when the attempts run out.** The first attempt runs
62
+ // before any wait, so a condition already met costs nothing — which is the common case for a tag that
63
+ // was created while the command was doing something else.
64
+ async function poll_until(
65
+ is_done: () => Promise<boolean> | boolean,
66
+ options: PollOptions,
67
+ ): Promise<boolean> {
68
+ const is_reached = await poll_for(async () => ((await is_done()) ? true : undefined), options)
69
+
70
+ return is_reached === true
71
+ }
72
+
73
+ // **A wait that ends only when `read` answers** — a child process exiting, a ship leaving `running`.
74
+ // Its bound, if any, is the read's own: a wall-clock deadline the read answers with its failure.
75
+ async function wait_for<T>(
76
+ read: () => Promise<T | undefined> | T | undefined,
77
+ options: WaitOptions,
78
+ ): Promise<T> {
79
+ const value = await poll_for(read, { ...options, attempts: Infinity })
80
+ if (value === undefined) throw new Error('an unbounded poll ran out of attempts')
81
+
82
+ return value
54
83
  }
55
84
 
56
- const poll = { poll_until, sleep }
85
+ const poll = { poll_for, poll_until, sleep, wait_for }
57
86
 
58
87
  export { poll }
59
- export type { PollOptions }
88
+ export type { PollOptions, WaitOptions }
@@ -0,0 +1,10 @@
1
+ // Both steps run to their end before either's failure is thrown, so neither is left half-done behind
2
+ // the other's failure. The first step's failure is thrown when both failed.
3
+ async function settled_together(first: Promise<void>, second: Promise<void>): Promise<void> {
4
+ const [result, other] = await Promise.allSettled([first, second])
5
+
6
+ if (result.status === 'rejected') throw result.reason
7
+ if (other.status === 'rejected') throw other.reason
8
+ }
9
+
10
+ export { settled_together }