@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
@@ -1,3 +1,5 @@
1
+ import { issue_cite } from '#scripts/issue/issue-cite'
2
+ import { session_cite } from '#scripts/issue/session-cite'
1
3
  import { run_cut, type CutState, type RunCut } from './run-cut'
2
4
  import { run_cut_handoff } from './run-cut-handoff'
3
5
 
@@ -136,6 +138,30 @@ function report_reviewer(): number {
136
138
  return report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE)
137
139
  }
138
140
 
141
+ // The cut outside a lane is taken, and nothing is relaunched: the session that took it is the one a
142
+ // person is watching, so the run is handed on the way `fullrun`'s entry `over` stop hands it on.
143
+ function report_held_cut(issue: string): number {
144
+ console.error(
145
+ `The cut is recorded for ${session_cite.issue(issue)} outside a lane, so nothing was relaunched. Keep the hold, send a \`confirmation\` Telegram whose body names the resume command \`fullrun ${issue_cite.plain(issue)}\`, and end the turn; the fresh session's \`pnpm josh run:cut --resume ${issue}\` answers \`resume-impl\` with the handoff.`,
146
+ )
147
+
148
+ return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
149
+ }
150
+
151
+ // **A cut that relaunched its successor says so on this session's own transcript** (joshuafolkken/kit#3936).
152
+ // The successor's `run:cut --resume` can clear the record before this session's turn ends, so the
153
+ // `Stop` hook cannot tell a cut stop by the record alone; it reads this line off the turn instead, a
154
+ // fact no other process can remove. The held cut never prints it — a person resumes that one. **A bare
155
+ // token line**, so it is read off the `run:cut` call's own result (`cost-run-cut.ts`), and a turn that
156
+ // merely quotes it is not read as a relaunch.
157
+ const RELAUNCHED_CUT_MARKER = 'cut-relaunched'
158
+
159
+ function report_relaunched_cut(): number {
160
+ console.error(RELAUNCHED_CUT_MARKER)
161
+
162
+ return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
163
+ }
164
+
139
165
  // **An implementation cut is never resumed by a session over the threshold**.
140
166
  // The cut outside a lane leaves the session that took it open, so a person retyping `fullrun #N` there
141
167
  // would be answered `resume-impl`, keep implementing over the threshold, and be cut again on the next
@@ -162,6 +188,7 @@ const run_cut_report = {
162
188
  INCOMPLETE_VERDICT,
163
189
  NOT_A_LANE_VERDICT,
164
190
  OVER_VERDICT,
191
+ RELAUNCHED_CUT_MARKER,
165
192
  RESUME_IMPL_VERDICT,
166
193
  RESUME_VERDICT,
167
194
  STALE_VERDICT,
@@ -176,8 +203,10 @@ const run_cut_report = {
176
203
  report_busy,
177
204
  report_fault,
178
205
  report_handed_off,
206
+ report_held_cut,
179
207
  report_incomplete,
180
208
  report_over,
209
+ report_relaunched_cut,
181
210
  report_reviewer,
182
211
  report_stale,
183
212
  report_under_threshold,
@@ -106,6 +106,8 @@ interface CutState {
106
106
  is_held: boolean
107
107
  // The issue a `fullrun` hold names, absent when the tree is not held by one.
108
108
  held_issue?: string | undefined
109
+ // When the hold standing over the tree was taken — what keys a declined cut to the run it answered.
110
+ hold_taken_at?: string | undefined
109
111
  }
110
112
 
111
113
  // The fields a cut is written from, bundled so `begin_cut` stays within the parameter limit. `phase`
@@ -402,7 +404,13 @@ async function current_state(): Promise<CutState> {
402
404
  const is_dirty = await run_hold.is_tree_dirty()
403
405
  const hold = hold_of(await worktree_directory())
404
406
 
405
- return { branch, is_dirty, is_held: hold !== undefined, held_issue: fullrun_issue(hold) }
407
+ return {
408
+ branch,
409
+ is_dirty,
410
+ is_held: hold !== undefined,
411
+ held_issue: fullrun_issue(hold),
412
+ hold_taken_at: hold?.taken_at,
413
+ }
406
414
  }
407
415
 
408
416
  function describe_cut(cut: RunCut): string {
@@ -466,6 +474,7 @@ const run_cut = {
466
474
  handed_off_message,
467
475
  has_required_handoff,
468
476
  held_issue_sync,
477
+ hold_of,
469
478
  incomplete_message,
470
479
  invocation_for,
471
480
  is_expired,
@@ -4,6 +4,7 @@ import { agent_launch_environment } from '#scripts/agent/agent-launch-environmen
4
4
  import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role-profile'
5
5
  import { agent_session_environment } from '#scripts/josh/agent-session-environment'
6
6
  import { stamp_file } from '#scripts/josh/stamp-file'
7
+ import { launch_priority } from '#scripts/run/launch-priority'
7
8
 
8
9
  // Starting a process that outlives this one, and keeping what it writes.
9
10
  //
@@ -34,13 +35,18 @@ const UNSAFE_NOTE =
34
35
  //
35
36
  // A NUL byte is what `execve` treats as the end of a string, so a value carrying one executes as a
36
37
  // prefix of itself; the other control characters have no business in a command line and their presence
37
- // means the value did not come from where it was supposed to.
38
+ // means the value did not come from where it was supposed to. **An agent's prompt is the one element a
39
+ // line break belongs in** — a ship-stop prompt carries the failed output's lines — so a vector marked
40
+ // `ends_with_prompt` lets it into its last element alone (joshuafolkken/kit#3964).
38
41
  //
39
42
  // The shared value validation lives with the role policy so environment overrides and argv elements
40
43
  // cannot disagree about which control characters or lengths are safe.
41
44
  interface LaunchArgv {
42
45
  command: string
43
46
  args: ReadonlyArray<string>
47
+ // Set by an agent argv builder, whose prompt is the last element: that one element may carry line
48
+ // breaks (`is_safe_prompt`), and every other element is checked as before.
49
+ ends_with_prompt?: boolean
44
50
  }
45
51
 
46
52
  type LaunchResult = { kind: 'launched'; pid: number } | { kind: 'failed'; note: string }
@@ -65,12 +71,23 @@ interface LaunchRequest {
65
71
  profile?: AgentProfile | undefined
66
72
  }
67
73
 
68
- const { is_safe_value } = agent_role_profile
74
+ const { is_safe_prompt, is_safe_value } = agent_role_profile
75
+ const LAST = -1
76
+
77
+ // An agent vector's prompt, the last element, may break lines; every other element is a value.
78
+ function is_safe_element(argv: LaunchArgv, value: string, index: number): boolean {
79
+ const is_prompt = argv.ends_with_prompt === true && index === argv.args.length + LAST
80
+
81
+ return is_prompt ? is_safe_prompt(value) : is_safe_value(value)
82
+ }
69
83
 
70
84
  // The single gate every argument vector passes before it reaches the operating system, whoever
71
85
  // composed it.
72
86
  function is_safe_argv(argv: LaunchArgv): boolean {
73
- return is_safe_value(argv.command) && argv.args.every((value) => is_safe_value(value))
87
+ return (
88
+ is_safe_value(argv.command) &&
89
+ argv.args.every((value, index) => is_safe_element(argv, value, index))
90
+ )
74
91
  }
75
92
 
76
93
  function note_of(error: unknown): string {
@@ -251,6 +268,7 @@ async function attached_spawn(
251
268
  child.once('exit', (exit_code) => {
252
269
  resolve({ kind: 'completed', pid: child.pid ?? 0, exit_code })
253
270
  })
271
+ launch_priority.lower(child.pid)
254
272
  notify_spawn(child.pid, on_spawn, on_error)
255
273
  })
256
274
  }
@@ -287,6 +305,7 @@ function spawned(
287
305
  on_error(note_of(error))
288
306
  })
289
307
  child.unref()
308
+ launch_priority.lower(child.pid)
290
309
 
291
310
  return child.pid === undefined ? { kind: 'failed', note: NO_PID_NOTE } : launched(child.pid)
292
311
  }
@@ -341,5 +360,5 @@ const detached_launch = {
341
360
  note_of,
342
361
  }
343
362
 
344
- export type { LaunchArgv, LaunchRequest, LaunchResult }
363
+ export type { AttachedLaunchResult, LaunchArgv, LaunchRequest, LaunchResult }
345
364
  export { detached_launch }
@@ -22,6 +22,7 @@ import {
22
22
  } from '#scripts/run/run-stage'
23
23
  import { run_stage_read, type StageRead } from '#scripts/run/run-stage-read'
24
24
  import { run_step } from '#scripts/run/run-step'
25
+ import { run_step_section } from '#scripts/run/run-step-section'
25
26
  import { run_entry, type EntryParts } from './run-entry'
26
27
  import { run_entry_stop } from './run-entry-stop'
27
28
 
@@ -259,7 +260,13 @@ async function proceed(issue_number: string, hold: string, cost: string): Promis
259
260
  // update is measured against (`issue-watch.ts`).
260
261
  await issue_watch.record_baseline_here(issue_number)
261
262
 
262
- return emit({ issue_number, hold, cost, verdict: reads.verdict, report: reads.report })
263
+ const code = emit({ issue_number, hold, cost, verdict: reads.verdict, report: reads.report })
264
+
265
+ // The entry's own read — the split assessment every run answers before its work — printed here so
266
+ // the reader does not spend a turn fetching it.
267
+ if (code !== SUCCESS_EXIT_CODE) return code
268
+
269
+ return run_step_section.print_section(run_step_section.ENTRY)
263
270
  }
264
271
 
265
272
  async function claim_run(
@@ -29,6 +29,7 @@ const KIND_LABELS: Readonly<Record<EventKind, KindLabel>> = {
29
29
  cut: { ja: 'セッション交代', en: 'session cut' },
30
30
  resume: { ja: '実装を再開', en: 'implementation resumed' },
31
31
  restart: { ja: '止めて出し直し', en: 'restarted' },
32
+ withdraw: { ja: '取り下げ', en: 'withdrawn' },
32
33
  drain: { ja: 'バックログが空', en: 'backlog drained' },
33
34
  stop: { ja: '停止', en: 'stopped' },
34
35
  'pr-opened': { ja: 'PR 作成', en: 'PR opened' },
@@ -94,6 +94,7 @@ const KIND = run_event_stream.EVENT_KIND
94
94
  const BATCH_RECORD_KINDS: ReadonlySet<string> = new Set([
95
95
  KIND.CHILD_LAUNCH,
96
96
  KIND.RESTART,
97
+ KIND.WITHDRAW,
97
98
  KIND.PARK,
98
99
  KIND.SPLIT,
99
100
  KIND.RETROSPECTIVE,
@@ -167,12 +168,14 @@ function last_issue_event(
167
168
  }
168
169
 
169
170
  // A child's launch and the endings `run:merge` settles it with — the ones `run:board` stops drawing it
170
- // running on. A park or a split already ran `run:merge`, so a child closed after one owes nothing.
171
+ // running on. A park or a split already ran `run:merge`, so a child closed after one owes nothing; a
172
+ // withdrawal settles the child itself, its issue closed by the person who withdrew it.
171
173
  const LAUNCH_OR_SETTLE_KINDS: ReadonlySet<string> = new Set([
172
174
  KIND.CHILD_LAUNCH,
173
175
  KIND.MERGE,
174
176
  KIND.PARK,
175
177
  KIND.SPLIT,
178
+ KIND.WITHDRAW,
176
179
  ])
177
180
 
178
181
  // Whether this invocation's newest launch of the issue has not yet been settled.
@@ -186,9 +189,24 @@ function is_merge_owed(events: ReadonlyArray<RunEvent>, scope: EventScope, issue
186
189
  return newest?.kind === KIND.CHILD_LAUNCH
187
190
  }
188
191
 
192
+ function is_launch_or_withdrawal(event: RunEvent, issue: string): boolean {
193
+ const is_kind = event.kind === KIND.CHILD_LAUNCH || event.kind === KIND.WITHDRAW
194
+
195
+ return is_kind && names_issue(event, issue)
196
+ }
197
+
198
+ // Whether a person withdrew the issue's newest launch (`lane:withdraw`): its newest launch or withdrawal
199
+ // is a withdrawal. A launch after it is a fresh attempt, which the old withdrawal does not settle.
200
+ function is_withdrawn(events: ReadonlyArray<RunEvent>, issue: string): boolean {
201
+ const newest = events.findLast((event) => is_launch_or_withdrawal(event, issue))
202
+
203
+ return newest?.kind === KIND.WITHDRAW
204
+ }
205
+
189
206
  const run_event_scope = {
190
207
  UNKNOWN_EVENT_SCOPE,
191
208
  is_merge_owed,
209
+ is_withdrawn,
192
210
  issue_named,
193
211
  last_issue_event,
194
212
  last_scoped_event,
@@ -113,10 +113,9 @@ async function emit_heartbeat(line: string): Promise<void> {
113
113
  await emit(run_event_stream.EVENT_KIND.HEARTBEAT, line)
114
114
  }
115
115
 
116
- // The events of the invocation now running, for a reader restoring the run from its stream
117
- // (`backlog:drive`). Fail-quiet like the writers: no repository or an unreadable stream is an empty
118
- // invocation.
119
- async function current_events(): Promise<ReadonlyArray<RunEvent>> {
116
+ // The events of the invocation now running, `undefined` where the invocation's scope could not be told
117
+ // — for a reader that must not take "no events" for "nothing launched" (`backlog-occupied.ts`).
118
+ async function scoped_current_events(): Promise<ReadonlyArray<RunEvent> | undefined> {
120
119
  try {
121
120
  const repository = await run_carry.repository_directory()
122
121
 
@@ -126,12 +125,19 @@ async function current_events(): Promise<ReadonlyArray<RunEvent>> {
126
125
  const scope = run_event_scope.scope_of(read)
127
126
  const events = run_event_stream.read_events(run_event_stream.target_of(repository))
128
127
 
129
- return run_event_scope.scoped_events(events, scope) ?? []
128
+ return run_event_scope.scoped_events(events, scope)
130
129
  } catch {
131
- return []
130
+ return undefined
132
131
  }
133
132
  }
134
133
 
134
+ // The events of the invocation now running, for a reader restoring the run from its stream
135
+ // (`backlog:drive`). Fail-quiet like the writers: no repository, an unreadable stream or an
136
+ // undetermined scope is an empty invocation.
137
+ async function current_events(): Promise<ReadonlyArray<RunEvent>> {
138
+ return (await scoped_current_events()) ?? []
139
+ }
140
+
135
141
  // The invocation's events for a writer deciding whether a marker is already there: unlike
136
142
  // `current_events`, an undetermined scope keeps the whole stream, so a run with no carry record skips a
137
143
  // marker it already wrote rather than writing it twice.
@@ -167,6 +173,7 @@ const run_event_stream_emit = {
167
173
  emit_once,
168
174
  emit_once_since,
169
175
  invocation_or_all_events,
176
+ scoped_current_events,
170
177
  stream_target,
171
178
  }
172
179
 
@@ -64,6 +64,10 @@ const EVENT_KIND = {
64
64
  // (`lane:restart`). It follows the fresh dispatch's `CHILD_LAUNCH`, so it is a launch to the parent and
65
65
  // the board, and — like that launch — a batch record a lane child never reads as its own position.
66
66
  RESTART: 'restart',
67
+ // A person withdrew a lane child — stopped it and closed its issue as not planned (`lane:withdraw`).
68
+ // It is written before the stop, so `run:merge` settles the child as withdrawn whatever GitHub says
69
+ // by the time the parent collects it, and counts it as neither a merge, a failure nor an outage.
70
+ WITHDRAW: 'withdraw',
67
71
  // The backlog emptied while nothing of the run's own was in flight — the drain. It marks the position
68
72
  // at which the end-of-run retrospective is owed, *before* the idle watch opens, so `run:step` fires
69
73
  // the retrospective at the drain rather than after the watch runs its course. It is not itself a
@@ -176,14 +176,25 @@ function held_block(hold: RunHold, issue: string): string | undefined {
176
176
  return run_hold.is_reentry(hold, issue) ? undefined : run_hold.held_message(hold)
177
177
  }
178
178
 
179
+ // An expired record of the claimant's own issue that no stop marked is the earlier run of the same work —
180
+ // a child killed and re-dispatched into its lane — so the claim adopts it with the work it left. A
181
+ // stopped `halfrun` or `prrun` keeps its own adopt path.
182
+ function is_own_unstopped(hold: RunHold, issue: string): boolean {
183
+ return run_hold.is_reentry(hold, issue) && !run_hold.is_stop_marked(hold)
184
+ }
185
+
186
+ async function stale_block(hold: RunHold, issue: string): Promise<string | undefined> {
187
+ if (is_own_unstopped(hold, issue)) return undefined
188
+
189
+ return (await run_hold.is_tree_dirty()) ? run_hold.uncommitted_message(hold) : undefined
190
+ }
191
+
179
192
  async function blocking_message(read: HoldRead, issue: string): Promise<string | undefined> {
180
193
  if (read.kind === 'held') return held_block(read.hold, issue)
181
194
 
182
195
  if (read.kind === 'unreadable') return run_hold.unreadable_message()
183
196
 
184
- const is_expired_over_work = read.kind === 'stale' && (await run_hold.is_tree_dirty())
185
-
186
- return is_expired_over_work ? run_hold.uncommitted_message(read.hold) : undefined
197
+ return read.kind === 'stale' ? await stale_block(read.hold, issue) : undefined
187
198
  }
188
199
 
189
200
  // A record already here is never overwritten by a claim: overwriting is what the guard exists to
@@ -0,0 +1,46 @@
1
+ import { stamp_file } from '#scripts/josh/stamp-file'
2
+ import { run_halfrun_resume } from '#scripts/run/run-halfrun-resume'
3
+ import { run_hold, type RunHold, type SessionOwner } from './run-hold'
4
+
5
+ // What the `Stop` hook asks of a hold beyond "is one in place" (joshuafolkken/kit#3961): whether it is
6
+ // this session's own run's, and whether its pause was already announced.
7
+ //
8
+ // **Another run's hold is not this session's pause.** A `kickoff`, or a question answered while a
9
+ // `backlogrun` parent holds the same tree, stops in a session that holds nothing — announcing that stop
10
+ // as the held run's "confirmation required" reports a decision nobody is waiting on.
11
+ //
12
+ // **Unknown is this session's own.** A record written outside an agent session, or a hook that cannot
13
+ // read its own session, keeps the behavior the hold had before ownership was recorded: a missed notify
14
+ // costs a person a silent pause, a wrong one only a needless Telegram.
15
+
16
+ function is_same_start(recorded: string | undefined, own: string | undefined): boolean {
17
+ return recorded === undefined || own === undefined || recorded === own
18
+ }
19
+
20
+ function is_session_hold(hold: RunHold, owner: SessionOwner = run_hold.session_owner()): boolean {
21
+ if (hold.owner_pid === undefined || owner.owner_pid === undefined) return true
22
+
23
+ return hold.owner_pid === owner.owner_pid && is_same_start(hold.owner_start, owner.owner_start)
24
+ }
25
+
26
+ // **A pause is announced once, and a person's prompt ends it.** The hook's own send is recorded on the
27
+ // hold, so a later stop of the same pause — a background notice that woke the session — finds it; a send before `pause_start` belongs to an earlier pause. An unknown start
28
+ // announces again: a needless Telegram costs less than a silent wait.
29
+ function is_notified_since(hold: RunHold, pause_start: string | undefined): boolean {
30
+ if (pause_start === undefined || hold.notified_at === undefined) return false
31
+
32
+ return Date.parse(hold.notified_at) >= Date.parse(pause_start)
33
+ }
34
+
35
+ // Stamps the send on the record in place; a tree no longer held has nothing to stamp. A stale record is
36
+ // stamped too: the hook reads it as the hold (`hold_of`), so a run past the expiry age still finds its send.
37
+ function mark_notified(target: string, now: Date = new Date()): void {
38
+ const hold = run_halfrun_resume.hold_of(run_hold.read_hold(target, now))
39
+ if (hold === undefined) return
40
+
41
+ stamp_file.write_stamp(target, { ...hold, notified_at: now.toISOString() })
42
+ }
43
+
44
+ const run_hold_session = { is_notified_since, is_session_hold, mark_notified }
45
+
46
+ export { run_hold_session }
@@ -65,6 +65,10 @@ interface RunHold {
65
65
  // fields, or by a claim made outside an agent session.
66
66
  owner_pid?: number | undefined
67
67
  owner_start?: string | undefined
68
+ // When the `Stop` hook last sent this run's `confirmation` Telegram — its own send leaves no command
69
+ // on the transcript, so the record is what keeps one pause from being announced twice
70
+ // (`run-hold-session.ts`).
71
+ notified_at?: string | undefined
68
72
  }
69
73
 
70
74
  type HoldRead =
@@ -107,6 +111,7 @@ const run_hold_schema = z.object({
107
111
  prrun_stop_head: z.string().optional(),
108
112
  owner_pid: z.number().optional(),
109
113
  owner_start: z.string().optional(),
114
+ notified_at: z.string().optional(),
110
115
  })
111
116
 
112
117
  // `undefined` for anything that is not a well-formed record, so the caller decides what a broken one
@@ -129,10 +134,12 @@ function is_expired(hold: RunHold, now: Date): boolean {
129
134
  // nothing. Only a recorded session that is provably gone counts — no owner, or a live pid whose start
130
135
  // cannot be read, leaves the age as the whole test. **A stop-marked record is exempt**: a `halfrun` or
131
136
  // `prrun` stop is held across a person's latency on purpose, long after its session ends.
132
- function is_owner_gone(hold: RunHold): boolean {
133
- const is_stop_mark = hold.is_halfrun_stop === true || hold.prrun_stop_head !== undefined
137
+ function is_stop_marked(hold: RunHold): boolean {
138
+ return hold.is_halfrun_stop === true || hold.prrun_stop_head !== undefined
139
+ }
134
140
 
135
- if (is_stop_mark || hold.owner_pid === undefined) return false
141
+ function is_owner_gone(hold: RunHold): boolean {
142
+ if (is_stop_marked(hold) || hold.owner_pid === undefined) return false
136
143
 
137
144
  return process_identity.is_same_process(hold.owner_pid, hold.owner_start) === false
138
145
  }
@@ -317,6 +324,7 @@ function unknown_message(): string {
317
324
  }
318
325
 
319
326
  const run_hold = {
327
+ is_stop_marked,
320
328
  FORCE_RELEASE_COMMAND,
321
329
  HOLD_MAX_AGE_HOURS,
322
330
  HOLD_MAX_AGE_MS,
@@ -339,6 +347,7 @@ const run_hold = {
339
347
  race_message,
340
348
  read_hold,
341
349
  release_hold,
350
+ session_owner,
342
351
  stale_message,
343
352
  uncommitted_message,
344
353
  unknown_message,
@@ -347,5 +356,5 @@ const run_hold = {
347
356
  write_hold,
348
357
  }
349
358
 
350
- export type { HoldRead, RunHold, StopMarkFields }
359
+ export type { HoldRead, RunHold, SessionOwner, StopMarkFields }
351
360
  export { run_hold }
@@ -0,0 +1,32 @@
1
+ import { setPriority } from 'node:os'
2
+
3
+ // **A launched session runs below the interactive one, so a saturated machine still answers the
4
+ // user.** Every headless session and everything it starts — gates, tests, builds — used to share the
5
+ // interactive session's priority, and at a load of ~30 the lanes took the terminal's responsiveness
6
+ // with them (#3947). The niceness is set on the child right after `spawn` returns: a process inherits
7
+ // its parent's niceness at fork, and an agent CLI boots for far longer than this takes before it forks
8
+ // anything, so every descendant inherits it. Efficiency-core confinement (`taskpolicy -b`) is not
9
+ // used — it throttles a lane far beyond yielding to the user.
10
+ const LANE_NICENESS = 10
11
+
12
+ type SetPriority = (pid: number, priority: number) => void
13
+
14
+ // **A priority that cannot be lowered starts the session as before.** A platform without the call, or
15
+ // a child that already runs at a lower priority than this, is not a reason to fail the launch.
16
+ function lower(pid: number | undefined, set_priority: SetPriority = setPriority): void {
17
+ if (pid === undefined) return
18
+
19
+ try {
20
+ set_priority(pid, LANE_NICENESS)
21
+ } catch {
22
+ // the session keeps the priority it was started with
23
+ }
24
+ }
25
+
26
+ const launch_priority = {
27
+ LANE_NICENESS,
28
+ lower,
29
+ }
30
+
31
+ export type { SetPriority }
32
+ export { launch_priority }
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
- import { api_outage } from '#scripts/agent/api-outage'
4
3
  import { CONTEXT_CUT_THRESHOLD } from '#scripts/cost-runtime/context-cut-threshold'
5
4
  import { issue_cite } from '#scripts/issue/issue-cite'
6
- import { issue_closing_pr } from '#scripts/issue/issue-closing-pr'
7
5
  import { issue_number_shape } from '#scripts/issue/issue-number-shape'
8
6
  import { issue_state_cli } from '#scripts/issue/issue-state-cli'
9
7
  import { lane_handoff } from '#scripts/lane/lane-handoff'
@@ -12,9 +10,9 @@ import { cli_flags } from '#scripts/lib/cli-flags'
12
10
  import { run_carry, type CarryOwner, type RunCarry } from '#scripts/run/carry/run-carry'
13
11
  import { run_event_stream } from '#scripts/run/event/run-event-stream'
14
12
  import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
15
- import { run_ending } from '#scripts/run/run-ending'
16
13
  import { run_label } from '#scripts/run/run-label'
17
- import { run_merge, type ChildOutcome, type EndingSignals } from './run-merge'
14
+ import { run_merge, type ChildOutcome } from './run-merge'
15
+ import { run_merge_signals } from './run-merge-signals'
18
16
  import { run_merge_steps, type FailedResult, type MergeContext } from './run-merge-steps'
19
17
  import { run_merge_token, type MergeToken } from './run-merge-token'
20
18
 
@@ -189,40 +187,6 @@ async function offered(ctx: MergeContext): Promise<OfferVerdict> {
189
187
  return { token: await run_merge_steps.ask_next(ctx), code: SUCCESS_EXIT_CODE, is_offer: true }
190
188
  }
191
189
 
192
- // Whether the child's exit record shows it could not reach the API — read only when `--output` named
193
- // the transcript, off otherwise. The path is validated inside
194
- // `run_ending.read_exit`, which returns `undefined` for an unreadable or missing record, so a missing
195
- // output is simply "not an outage".
196
- function read_is_outage(output: string | undefined): boolean {
197
- if (output === undefined) return false
198
-
199
- return api_outage.is_outage(run_ending.read_exit(output))
200
- }
201
-
202
- type IssueRead = Awaited<ReturnType<typeof issue_state_cli.read_issue>>
203
-
204
- // The timeline is read only for an OPEN, unparked child — the one classification a merged closing pull
205
- // request changes. A CLOSED child was closed by GitHub, so it must carry no `merged_pr` for
206
- // `do_merged` to close again.
207
- async function read_merged_pr(child: string, read: IssueRead): Promise<string | undefined> {
208
- if (read.kind !== 'state' || run_merge.classify_child(read.state) !== 'failed') return undefined
209
-
210
- return await issue_closing_pr.read_closing_pr(child)
211
- }
212
-
213
- async function read_signals(ctx: MergeContext, read: IssueRead): Promise<EndingSignals> {
214
- const [is_cut, merged_pr] = await Promise.all([
215
- run_merge_steps.has_resumable_cut(ctx.child),
216
- read_merged_pr(ctx.child, read),
217
- ])
218
-
219
- return { is_outage: read_is_outage(ctx.output), is_cut, merged_pr }
220
- }
221
-
222
- function outcome_of(read: IssueRead, signals: EndingSignals): ChildOutcome {
223
- return read.kind === 'state' ? run_merge.classify_child(read.state, signals) : 'unresolved'
224
- }
225
-
226
190
  // A refused count means this session is not the carry record's owner, so the whole operation is
227
191
  // rejected — the lane is not closed and no next child is offered.
228
192
  function report_count_refused(carry: RunCarry | undefined): MergeVerdict {
@@ -374,6 +338,13 @@ async function on_skipped(ctx: MergeContext): Promise<MergeVerdict> {
374
338
  return await offered(ctx)
375
339
  }
376
340
 
341
+ // A child a person withdrew with `lane:withdraw`: the withdrawal already stopped it, closed its issue
342
+ // and lane and put itself on the stream, so nothing is counted or written here and its seat goes to the
343
+ // next child.
344
+ async function on_withdrawn(ctx: MergeContext): Promise<MergeVerdict> {
345
+ return await offered(ctx)
346
+ }
347
+
377
348
  // The child's own ending: it stopped before its commit for a person to look at, so nothing is counted
378
349
  // and no next child is offered.
379
350
  async function on_human_review(): Promise<MergeVerdict> {
@@ -399,6 +370,7 @@ const HANDLERS: Readonly<Record<ChildOutcome, (ctx: MergeContext) => Promise<Mer
399
370
  split: on_skipped,
400
371
  unresolved: on_unresolved,
401
372
  shipping: on_shipping,
373
+ withdrawn: on_withdrawn,
402
374
  }
403
375
 
404
376
  // The whole merge event for one returned child, in-process: read its state, classify it, and run the
@@ -409,10 +381,12 @@ async function merge_child(ctx: MergeContext): Promise<MergeResult> {
409
381
  }
410
382
 
411
383
  const read = await issue_state_cli.read_issue(ctx.child, ctx.repo)
412
- const signals = await read_signals(ctx, read)
413
- const outcome = outcome_of(read, signals)
384
+ const signals = await run_merge_signals.read_signals(ctx, read)
385
+ const outcome = run_merge_signals.outcome_of(read, signals)
386
+
387
+ const { merged_pr, is_unanswered } = signals
414
388
 
415
- return { outcome, ...(await HANDLERS[outcome]({ ...ctx, merged_pr: signals.merged_pr })) }
389
+ return { outcome, ...(await HANDLERS[outcome]({ ...ctx, merged_pr, is_unanswered })) }
416
390
  }
417
391
 
418
392
  async function run(argv: ReadonlyArray<string>): Promise<number> {
@@ -438,7 +412,7 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
438
412
  const run_merge_cli = {
439
413
  merge_child,
440
414
  parse,
441
- read_merged_pr,
415
+ read_merged_pr: run_merge_signals.read_merged_pr,
442
416
  record_merged,
443
417
  run,
444
418
  }
@@ -1,6 +1,9 @@
1
1
  import { git_common_directory } from '#scripts/git/git-common-directory'
2
+ import { lane_registry } from '#scripts/lane/lane-registry'
3
+ import { lane_relaunch } from '#scripts/lane/lane-relaunch'
2
4
  import { run_event_scope } from '#scripts/run/event/run-event-scope'
3
5
  import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
6
+ import { run_ending } from '#scripts/run/run-ending'
4
7
  import { run_ship_detach } from '#scripts/run/ship/run-ship-detach'
5
8
 
6
9
  // Why `run:merge` parks a child whose detached ship has already ended (joshuafolkken/kit#3808).
@@ -56,6 +59,61 @@ function cause(child: string, directory: string = process.cwd()): string | undef
56
59
  return cause_of(run_ship_detach.read_result(repository, child), events, child)
57
60
  }
58
61
 
59
- const run_merge_ship = { cause, cause_of }
62
+ // Whether the ended ship was never answered: a session relaunched to fix a stop writes to the
63
+ // transcript after it, so a transcript last written before the newest stop means no fix-up ran — its
64
+ // broker or relaunch could not start. A ship with no stop at all ended abnormally only when its supervisor
65
+ // died before reaching a stage.
66
+ function is_stop_unanswered(
67
+ ending: ShipEnding,
68
+ attempt: Events,
69
+ written_at: number | undefined,
70
+ ): boolean {
71
+ const stop = attempt.findLast((event) => event.kind === run_event_stream.EVENT_KIND.SHIP_STOP)
72
+
73
+ if (stop === undefined) return ending?.result === 'abnormal'
74
+
75
+ return written_at !== undefined && written_at < Date.parse(stop.at)
76
+ }
77
+
78
+ // Whether the child's ship ended through an interruption rather than its work, so the child is
79
+ // re-dispatched instead of parked.
80
+ function interrupted_of(
81
+ ending: ShipEnding,
82
+ events: Events,
83
+ child: string,
84
+ written_at: number | undefined,
85
+ ): boolean {
86
+ if (ending === undefined || !ENDED_RESULTS.has(ending.result)) return false
87
+
88
+ const attempt = since_ship_launch(events, child)
89
+
90
+ return attempt !== undefined && is_stop_unanswered(ending, attempt, written_at)
91
+ }
92
+
93
+ function is_interrupted(
94
+ child: string,
95
+ written_at: number | undefined,
96
+ directory: string = process.cwd(),
97
+ ): boolean {
98
+ const repository = git_common_directory.repository(directory)
99
+ if (repository === undefined) return false
100
+
101
+ const events = run_event_stream.read_events(run_event_stream.target_of(repository))
102
+
103
+ return interrupted_of(run_ship_detach.read_result(repository, child), events, child, written_at)
104
+ }
105
+
106
+ // Whether the child's ship stopped and the relaunch meant to fix it never ran, read against its
107
+ // transcript at `output`. An OpenAI lane is never relaunched — its parent books every stop as an ended
108
+ // child — and nothing relaunches a child whose lane is gone, so only an open non-OpenAI lane reads as one.
109
+ async function is_unanswered(child: string, output: string | undefined): Promise<boolean> {
110
+ if (output === undefined || !is_interrupted(child, run_ending.written_at(output))) return false
111
+
112
+ const lane = await lane_registry.find_open_lane(child)
113
+
114
+ return lane !== undefined && !lane_relaunch.is_openai_lane(lane)
115
+ }
116
+
117
+ const run_merge_ship = { cause, cause_of, interrupted_of, is_unanswered }
60
118
 
61
119
  export { run_merge_ship }