@joshuafolkken/kit 1.1455.0 → 1.1514.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/.claude/settings.json +6 -6
  2. package/.claude/skills/dependency-update/SKILL.md +26 -55
  3. package/.claude/skills/epic-commands/SKILL.md +83 -156
  4. package/.claude/skills/epic-commands/epic-bundle.md +51 -79
  5. package/.claude/skills/epic-commands/execution-waves.md +43 -81
  6. package/.claude/skills/verify-ui/SKILL.md +24 -50
  7. package/.claude/skills/workflow-commands/SKILL.md +7 -9
  8. package/.claude/skills/workflow-commands/background-commands.md +31 -46
  9. package/.claude/skills/workflow-commands/backlogrun-child.md +65 -85
  10. package/.claude/skills/workflow-commands/backlogrun-lanes.md +86 -125
  11. package/.claude/skills/workflow-commands/backlogrun-park.md +68 -118
  12. package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -28
  13. package/.claude/skills/workflow-commands/backlogrun-recovery.md +17 -26
  14. package/.claude/skills/workflow-commands/backlogrun-steps.md +35 -52
  15. package/.claude/skills/workflow-commands/backlogrun.md +31 -50
  16. package/.claude/skills/workflow-commands/chain-rule.md +31 -40
  17. package/.claude/skills/workflow-commands/delegation.md +12 -8
  18. package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
  19. package/.claude/skills/workflow-commands/followup.md +55 -77
  20. package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
  21. package/.claude/skills/workflow-commands/fullrun.md +4 -6
  22. package/.claude/skills/workflow-commands/halfrun.md +3 -7
  23. package/.claude/skills/workflow-commands/into-target.md +1 -2
  24. package/.claude/skills/workflow-commands/issue-comments.md +43 -60
  25. package/.claude/skills/workflow-commands/issue-scout.md +3 -2
  26. package/.claude/skills/workflow-commands/kickoff.md +65 -74
  27. package/.claude/skills/workflow-commands/latest-gate.md +2 -6
  28. package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
  29. package/.claude/skills/workflow-commands/observation-filing.md +59 -118
  30. package/.claude/skills/workflow-commands/observation-ledger.md +52 -112
  31. package/.claude/skills/workflow-commands/pre-gate-cut.md +88 -142
  32. package/.claude/skills/workflow-commands/prerequisite.md +11 -17
  33. package/.claude/skills/workflow-commands/progress-watcher.md +23 -33
  34. package/.claude/skills/workflow-commands/prrun.md +3 -5
  35. package/.claude/skills/workflow-commands/retrospective.md +35 -54
  36. package/.claude/skills/workflow-commands/split-assessment.md +32 -58
  37. package/.claude/skills/workflow-commands/target-repository.md +1 -2
  38. package/.claude/skills/workflow-commands/working-tree-hold.md +54 -73
  39. package/.codex/hooks.json +6 -6
  40. package/.github/actions/setup-node/action.yml +1 -1
  41. package/.github/workflows/ci.yml +1 -1
  42. package/.github/workflows/publish.yml +2 -2
  43. package/CLAUDE.md +7 -6
  44. package/cspell/index.yaml +7 -0
  45. package/dist/CLAUDE.md +7 -6
  46. package/dist/commands/document-section-cli.js +67 -34
  47. package/dist/hooks/batch-guard.js +6 -6
  48. package/dist/hooks/chunk-4KKD2UTK.js +66 -0
  49. package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-6TS2JD6D.js} +14 -3
  50. package/dist/hooks/chunk-7DSBJVFH.js +50 -0
  51. package/dist/hooks/{chunk-5H4SSDSD.js → chunk-A65Y4YOR.js} +3 -3
  52. package/dist/hooks/{chunk-3C4XRYG6.js → chunk-APUO6U4J.js} +102 -19
  53. package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
  54. package/dist/hooks/chunk-GP4IEC4O.js +20 -0
  55. package/dist/hooks/chunk-JP45SOCJ.js +56 -0
  56. package/dist/hooks/{chunk-OLMO72ON.js → chunk-LAOV7NLM.js} +1 -1
  57. package/dist/hooks/chunk-M4HGZV7U.js +83 -0
  58. package/dist/hooks/{chunk-TBJFTSUY.js → chunk-O6GDRNJB.js} +9 -7
  59. package/dist/hooks/{chunk-HFGWYGUV.js → chunk-PF2NZL2A.js} +5 -5
  60. package/dist/hooks/{chunk-WNLVGT7E.js → chunk-PTICX4OL.js} +4 -5
  61. package/dist/hooks/{chunk-HHI6FO3R.js → chunk-RT27QRJW.js} +763 -1515
  62. package/dist/hooks/{chunk-7WQBPLUN.js → chunk-SYOTDYCI.js} +1149 -1095
  63. package/dist/hooks/{chunk-LIAFCXL7.js → chunk-TNUMY6NL.js} +1519 -1332
  64. package/dist/hooks/chunk-UPUZF3AN.js +279 -0
  65. package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-WK6SMBIC.js} +1932 -784
  66. package/dist/hooks/codex-hook-adapter.js +17 -14
  67. package/dist/hooks/duplicate-read-guard.js +7 -7
  68. package/dist/hooks/format-edited.js +5 -4
  69. package/dist/hooks/inputs.json +302 -247
  70. package/dist/hooks/investigation-guard.js +8 -11
  71. package/dist/hooks/pretool-guard.js +16 -13
  72. package/dist/hooks/session-lang.js +372 -26
  73. package/dist/hooks/stop-guard.js +2593 -189
  74. package/dist/josh.js +2128 -282
  75. package/dist/managed-marker/index.js +1 -2
  76. package/dist/version/index.js +15 -12
  77. package/eslint/base.js +2 -1
  78. package/eslint/rules/unicorn.js +16 -0
  79. package/lefthook/base.yml +5 -3
  80. package/package.json +14 -12
  81. package/prompts/coding-standards.md +12 -22
  82. package/prompts/collaboration-workflow/file-edits.md +25 -45
  83. package/prompts/collaboration-workflow/gh-rest.md +19 -36
  84. package/prompts/collaboration-workflow/glossary.md +5 -7
  85. package/prompts/collaboration-workflow/issue-citation.md +6 -14
  86. package/prompts/collaboration-workflow/issue-template.md +40 -32
  87. package/prompts/collaboration-workflow/lane-instructions.md +35 -0
  88. package/prompts/collaboration-workflow/operating-rules.md +33 -46
  89. package/prompts/collaboration-workflow/output-bounds.md +15 -15
  90. package/prompts/collaboration-workflow/overview.md +14 -19
  91. package/prompts/collaboration-workflow/plan-comment.md +13 -13
  92. package/prompts/collaboration-workflow/principles.md +57 -54
  93. package/prompts/collaboration-workflow/proposal-request.md +1 -9
  94. package/prompts/collaboration-workflow/report-format.md +41 -49
  95. package/prompts/collaboration-workflow/residency.md +15 -11
  96. package/prompts/collaboration-workflow/rule-delivery.md +7 -15
  97. package/prompts/collaboration-workflow/shell-body.md +10 -20
  98. package/prompts/collaboration-workflow/turn-batching.md +8 -14
  99. package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
  100. package/prompts/collaboration-workflow/wip-cap.md +26 -32
  101. package/prompts/collaboration-workflow.md +25 -25
  102. package/prompts/refactoring.md +33 -43
  103. package/prompts/review-rubric.md +84 -129
  104. package/prompts/review.md +110 -141
  105. package/prompts/sonar-hotspot-handling.md +22 -50
  106. package/prompts/testing-guide.md +48 -99
  107. package/scripts/adopt/adopt-toolkits.ts +2 -4
  108. package/scripts/agent/agent-argv.ts +20 -4
  109. package/scripts/agent/agent-exit-record.ts +23 -1
  110. package/scripts/agent/agent-role-profile.ts +101 -45
  111. package/scripts/agent/agent-safe-text.ts +60 -0
  112. package/scripts/agent/claude-agent-argv.ts +14 -0
  113. package/scripts/agent/codex-agent-argv.ts +9 -2
  114. package/scripts/agent/lane-instructions.ts +144 -0
  115. package/scripts/backlog/backlog-budget-cli.ts +1 -2
  116. package/scripts/backlog/backlog-budget.ts +4 -4
  117. package/scripts/backlog/backlog-drive-carried.ts +26 -0
  118. package/scripts/backlog/backlog-drive-cli.ts +10 -5
  119. package/scripts/backlog/backlog-drive-named.ts +3 -5
  120. package/scripts/backlog/backlog-drive-restore.ts +28 -2
  121. package/scripts/backlog/backlog-drive-seats.ts +27 -0
  122. package/scripts/backlog/backlog-drive.ts +11 -6
  123. package/scripts/backlog/backlog-occupied.ts +56 -0
  124. package/scripts/backlog/backlog-ready.ts +59 -15
  125. package/scripts/behavior/behavior-rules.ts +3 -2
  126. package/scripts/cost-runtime/codex-usage.ts +4 -2
  127. package/scripts/cost-runtime/cost-cli.ts +1 -2
  128. package/scripts/delegation/duplicate-read-guard.ts +2 -2
  129. package/scripts/delegation/investigation-guard.ts +15 -65
  130. package/scripts/delegation/investigation-reads.ts +1 -2
  131. package/scripts/discovery/repo-discovery.ts +4 -2
  132. package/scripts/discovery/repo-origin.ts +2 -4
  133. package/scripts/doctor/doctor-logic.ts +6 -3
  134. package/scripts/document/document-byte-budget.ts +28 -27
  135. package/scripts/document/document-byte-check.ts +1 -2
  136. package/scripts/document/document-section-cli.ts +17 -5
  137. package/scripts/document/entry-read-budget.ts +3 -3
  138. package/scripts/document/read-set-cli.ts +4 -1
  139. package/scripts/epic/epic-audit-orphans.ts +5 -8
  140. package/scripts/epic/epic-audit-rationale.ts +8 -2
  141. package/scripts/epic/epic-audit.ts +52 -1
  142. package/scripts/epic/epic-bundle-cli.ts +1 -2
  143. package/scripts/epic/epic-bundle-evidence.ts +49 -26
  144. package/scripts/epic/epic-bundle-referenced.ts +14 -3
  145. package/scripts/epic/epic-bundle.ts +17 -9
  146. package/scripts/epic/epic-chains.ts +6 -2
  147. package/scripts/epic/epic-close-comment.ts +1 -2
  148. package/scripts/followup/git-followup-workflow.ts +19 -1
  149. package/scripts/gate/check-inputs.ts +129 -0
  150. package/scripts/gate/check-reuse.ts +162 -0
  151. package/scripts/gate/core-budget.ts +93 -44
  152. package/scripts/gate/core-wait.ts +130 -0
  153. package/scripts/gate/free-cores.ts +49 -0
  154. package/scripts/gate/gate-ledger.ts +17 -3
  155. package/scripts/gate/gate-plan.ts +23 -1
  156. package/scripts/gate/gate-report.ts +15 -2
  157. package/scripts/gate/gate-run-steps.ts +150 -0
  158. package/scripts/gate/held-mark.ts +55 -0
  159. package/scripts/gate/machine-capacity.ts +70 -11
  160. package/scripts/gate/pre-commit-type-check.ts +14 -1
  161. package/scripts/gate/verification-gate.ts +65 -127
  162. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  163. package/scripts/gh/git-gh-issue-write.ts +24 -2
  164. package/scripts/gh/git-pr.ts +19 -3
  165. package/scripts/git/git-common-directory.ts +3 -3
  166. package/scripts/git/git-diff-reads.ts +16 -9
  167. package/scripts/git/git-execa-error.ts +1 -2
  168. package/scripts/git/main-merge-regenerate.ts +78 -0
  169. package/scripts/git/main-merge.ts +14 -5
  170. package/scripts/hooks/batch-guard.ts +5 -8
  171. package/scripts/hooks/format-edited-file.ts +10 -4
  172. package/scripts/hooks/pre-commit-lint.ts +91 -0
  173. package/scripts/hooks/pre-push-unit.ts +15 -4
  174. package/scripts/hooks/pretool-guard.ts +48 -79
  175. package/scripts/hooks/reserved-run.ts +1 -0
  176. package/scripts/hooks/run-hook.sh +10 -12
  177. package/scripts/hooks/step-zero-notice.ts +3 -2
  178. package/scripts/hooks/stop-actions.ts +204 -0
  179. package/scripts/hooks/stop-guard.ts +62 -21
  180. package/scripts/init/hook-command-rewrite.ts +6 -2
  181. package/scripts/init/hook-launch.ts +12 -7
  182. package/scripts/init/init-logic-deploy-vps.ts +4 -2
  183. package/scripts/issue/behavior-change-lint.ts +12 -6
  184. package/scripts/issue/expected-effect-lint.ts +59 -0
  185. package/scripts/issue/issue-cite-cli.ts +35 -15
  186. package/scripts/issue/issue-classification.ts +1 -1
  187. package/scripts/issue/issue-lint-cli.ts +5 -3
  188. package/scripts/issue/issue-scout-cli.ts +22 -16
  189. package/scripts/issue/markdown-section.ts +19 -5
  190. package/scripts/josh/command-usage.ts +110 -0
  191. package/scripts/josh/file-map-stamp.ts +1 -3
  192. package/scripts/josh/hook-decision.ts +17 -2
  193. package/scripts/josh/josh-command-map.ts +2 -0
  194. package/scripts/josh/josh-command-types.ts +55 -9
  195. package/scripts/josh/josh-commands-ai.ts +13 -0
  196. package/scripts/josh/josh-commands-development.ts +1 -1
  197. package/scripts/josh/josh-commands-hooks.ts +6 -0
  198. package/scripts/josh/josh-commands-lane.ts +10 -1
  199. package/scripts/josh/josh-commands-workflow.ts +5 -0
  200. package/scripts/josh/josh-logic.ts +6 -15
  201. package/scripts/josh/josh-usage-automation.ts +121 -0
  202. package/scripts/josh/josh-usage-backlog.ts +180 -0
  203. package/scripts/josh/josh-usage-developer.ts +230 -0
  204. package/scripts/josh/josh-usage-hooks.ts +38 -0
  205. package/scripts/josh/josh-usage-issue.ts +205 -0
  206. package/scripts/josh/josh-usage-review.ts +101 -0
  207. package/scripts/josh/josh-usage-run.ts +252 -0
  208. package/scripts/josh/josh-usage-session.ts +238 -0
  209. package/scripts/josh/josh-usage-workflow.ts +212 -0
  210. package/scripts/josh/josh.ts +14 -5
  211. package/scripts/josh/package-directory.ts +20 -0
  212. package/scripts/lane/lane-await.ts +3 -2
  213. package/scripts/lane/lane-capacity.ts +8 -2
  214. package/scripts/lane/lane-check-stats.ts +56 -0
  215. package/scripts/lane/lane-child-invocation.ts +49 -5
  216. package/scripts/lane/lane-dispatch.ts +3 -3
  217. package/scripts/lane/lane-guard-policy.ts +5 -5
  218. package/scripts/lane/lane-handoff.ts +8 -1
  219. package/scripts/lane/lane-install.ts +12 -4
  220. package/scripts/lane/lane-ledger.ts +52 -1
  221. package/scripts/lane/lane-limit-cli.ts +8 -9
  222. package/scripts/lane/lane-load.ts +9 -2
  223. package/scripts/lane/lane-measure-cli.ts +4 -0
  224. package/scripts/lane/lane-parked.ts +18 -0
  225. package/scripts/lane/lane-preserve.ts +29 -1
  226. package/scripts/lane/lane-process-load.ts +125 -0
  227. package/scripts/lane/lane-process-stats.ts +93 -0
  228. package/scripts/lane/lane-registry.ts +17 -10
  229. package/scripts/lane/lane-restart.ts +5 -26
  230. package/scripts/lane/lane-session-action.ts +146 -0
  231. package/scripts/lane/lane-session-broker.ts +68 -125
  232. package/scripts/lane/lane-spent.ts +14 -3
  233. package/scripts/lane/lane-stats.ts +1 -0
  234. package/scripts/lane/lane-withdraw-cli.ts +71 -0
  235. package/scripts/lane/lane-withdraw.ts +182 -0
  236. package/scripts/lint/format.ts +4 -4
  237. package/scripts/lint/lint-parallel.ts +10 -3
  238. package/scripts/lint/lint-related-scope.ts +4 -1
  239. package/scripts/managed-marker/managed-marker-logic.ts +1 -2
  240. package/scripts/notify/telegram-test.ts +10 -5
  241. package/scripts/overrides/lockfile-overrides.ts +1 -3
  242. package/scripts/propagate/propagate.ts +1 -2
  243. package/scripts/release/release-progress.ts +3 -2
  244. package/scripts/report/report-format-reference.ts +2 -0
  245. package/scripts/report/report-lint.ts +7 -4
  246. package/scripts/review/acceptance-map-check.ts +61 -0
  247. package/scripts/review/acceptance-map.ts +205 -0
  248. package/scripts/review/review-brief-cli.ts +1 -1
  249. package/scripts/review/review-record-cli.ts +6 -2
  250. package/scripts/rules/agent-model.ts +59 -0
  251. package/scripts/rules/delivered-rules.ts +11 -2
  252. package/scripts/rules/destructive-command.ts +6 -9
  253. package/scripts/rules/direct-filing.ts +3 -7
  254. package/scripts/rules/early-heartbeat.ts +7 -18
  255. package/scripts/rules/file-body.ts +6 -13
  256. package/scripts/rules/filing-cap.ts +4 -6
  257. package/scripts/rules/firing-point.ts +6 -5
  258. package/scripts/rules/git-force.ts +5 -9
  259. package/scripts/rules/guard-remedy.ts +117 -0
  260. package/scripts/rules/implementation-cut.ts +37 -35
  261. package/scripts/rules/index-guard.ts +4 -7
  262. package/scripts/rules/issue-comments.ts +67 -16
  263. package/scripts/rules/josh-git-bare.ts +3 -6
  264. package/scripts/rules/lane-background.ts +10 -21
  265. package/scripts/rules/lane-carry-conflict.ts +7 -17
  266. package/scripts/rules/lane-interactive-ask.ts +6 -11
  267. package/scripts/rules/lane-park.ts +5 -13
  268. package/scripts/rules/lane-split-park.ts +2 -7
  269. package/scripts/rules/lane-switch-main.ts +9 -12
  270. package/scripts/rules/last-prompt.ts +50 -5
  271. package/scripts/rules/oracle-consulted.ts +4 -5
  272. package/scripts/rules/oracle-list-cli.ts +7 -0
  273. package/scripts/rules/piped-verification.ts +84 -32
  274. package/scripts/rules/poll-loop.ts +7 -17
  275. package/scripts/rules/pre-gate-cut.ts +7 -21
  276. package/scripts/rules/prior-comment-read.ts +7 -5
  277. package/scripts/rules/protected-files.ts +5 -9
  278. package/scripts/rules/raw-field-body.ts +3 -7
  279. package/scripts/rules/rule-body-guard.ts +4 -11
  280. package/scripts/rules/rule-list.ts +4 -1
  281. package/scripts/rules/rule-registry-rows.ts +10 -0
  282. package/scripts/rules/rule-registry.ts +7 -4
  283. package/scripts/rules/rule-rewrite.ts +114 -0
  284. package/scripts/rules/rule-value.ts +7 -1
  285. package/scripts/rules/run-tail-rule.ts +6 -22
  286. package/scripts/rules/shell-body-trigger.ts +4 -6
  287. package/scripts/rules/shell-segments.ts +22 -0
  288. package/scripts/rules/stop-decision.ts +77 -0
  289. package/scripts/rules/stop-rules.ts +164 -77
  290. package/scripts/rules/tail-commands.ts +15 -1
  291. package/scripts/rules/test-declared-commit.ts +4 -10
  292. package/scripts/rules/third-party-write.ts +5 -10
  293. package/scripts/rules/worktree-guard.ts +5 -12
  294. package/scripts/run/board/run-board-cli.ts +2 -0
  295. package/scripts/run/board/run-board-closed.ts +8 -2
  296. package/scripts/run/board/run-board-github.ts +19 -3
  297. package/scripts/run/board/run-board-header.ts +4 -1
  298. package/scripts/run/board/run-board-idle.ts +31 -0
  299. package/scripts/run/board/run-board-labels.ts +2 -0
  300. package/scripts/run/board/run-board-layout.ts +1 -1
  301. package/scripts/run/board/run-board-read.ts +25 -12
  302. package/scripts/run/board/run-board-reason.ts +17 -4
  303. package/scripts/run/board/run-board-render-legend.ts +1 -0
  304. package/scripts/run/board/run-board-render-slot.ts +1 -1
  305. package/scripts/run/board/run-board-render-waits.ts +41 -0
  306. package/scripts/run/board/run-board-render.ts +6 -1
  307. package/scripts/run/board/run-board-row-yield.ts +1 -0
  308. package/scripts/run/board/run-board-state.ts +9 -0
  309. package/scripts/run/board/run-board-status.ts +89 -23
  310. package/scripts/run/board/run-board-thanks.ts +7 -2
  311. package/scripts/run/board/run-board-tick.ts +37 -8
  312. package/scripts/run/carry/run-carry-args.ts +1 -3
  313. package/scripts/run/carry/run-carry-conversation.ts +7 -2
  314. package/scripts/run/cut/run-cut-cli.ts +18 -22
  315. package/scripts/run/cut/run-cut-decline.ts +72 -0
  316. package/scripts/run/cut/run-cut-ready.ts +74 -0
  317. package/scripts/run/cut/run-cut-report.ts +29 -0
  318. package/scripts/run/cut/run-cut.ts +10 -1
  319. package/scripts/run/detached-launch.ts +23 -4
  320. package/scripts/run/entry/run-entry-cli.ts +8 -1
  321. package/scripts/run/event/run-event-render.ts +1 -0
  322. package/scripts/run/event/run-event-scope.ts +19 -1
  323. package/scripts/run/event/run-event-stream-emit.ts +13 -6
  324. package/scripts/run/event/run-event-stream.ts +4 -0
  325. package/scripts/run/hold/run-hold-cli.ts +14 -3
  326. package/scripts/run/hold/run-hold-session.ts +46 -0
  327. package/scripts/run/hold/run-hold.ts +13 -4
  328. package/scripts/run/launch-priority.ts +32 -0
  329. package/scripts/run/merge/run-merge-cli.ts +16 -42
  330. package/scripts/run/merge/run-merge-ship.ts +59 -1
  331. package/scripts/run/merge/run-merge-signals.ts +99 -0
  332. package/scripts/run/merge/run-merge-steps.ts +11 -1
  333. package/scripts/run/merge/run-merge.ts +22 -9
  334. package/scripts/run/run-ending.ts +31 -3
  335. package/scripts/run/run-liveness.ts +1 -1
  336. package/scripts/run/run-parent-cut-hook.ts +5 -10
  337. package/scripts/run/run-poll-hook.ts +157 -0
  338. package/scripts/run/run-preflight.ts +1 -2
  339. package/scripts/run/run-step-cli.ts +22 -15
  340. package/scripts/run/run-step-section.ts +85 -0
  341. package/scripts/run/run-stop-notify.ts +1 -2
  342. package/scripts/run/run-watcher-guard.ts +6 -3
  343. package/scripts/run/ship/run-ship-cli.ts +5 -12
  344. package/scripts/run/ship/run-ship-detach.ts +9 -0
  345. package/scripts/run/ship/run-ship-next.ts +10 -2
  346. package/scripts/run/ship/run-ship-overlap.ts +195 -0
  347. package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
  348. package/scripts/run/ship/run-ship-preflight.ts +28 -2
  349. package/scripts/run/ship/run-ship-return.ts +31 -7
  350. package/scripts/run/ship/run-ship-review-steps.ts +25 -14
  351. package/scripts/run/ship/run-ship-review.ts +15 -2
  352. package/scripts/run/ship/run-ship-steps.ts +1 -1
  353. package/scripts/run/ship/run-ship-stop-context.ts +206 -0
  354. package/scripts/run/ship/run-ship-sync.ts +14 -6
  355. package/scripts/run/tidy/run-tidy-cli.ts +3 -3
  356. package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
  357. package/scripts/safe-chain/project-config.ts +7 -2
  358. package/scripts/sync/sync-hook-safety.ts +8 -2
  359. package/scripts/test/test-e2e-guard.ts +30 -5
  360. package/scripts/test/test-unit-guard.ts +16 -1
  361. package/scripts/test/unit-worker-share.ts +26 -3
  362. package/scripts/time-runtime/time-batch-guard.ts +3 -3
  363. package/scripts/time-runtime/time-hook-transcript.ts +3 -2
  364. package/scripts/time-runtime/time-shell.ts +39 -4
  365. package/scripts/time-runtime/time-spans.ts +1 -0
  366. package/scripts/time-runtime/time-tool-call.ts +7 -0
  367. package/scripts/version/effective-upstream.ts +3 -4
  368. package/scripts/version/kit-version-config.ts +8 -2
  369. package/scripts/version/latest-corepack.ts +1 -2
  370. package/scripts/version/release-hold.ts +11 -4
  371. package/scripts/version/version-commands.ts +12 -4
  372. package/templates/gitignore +6 -2
  373. package/dist/hooks/chunk-D5QBTOCZ.js +0 -68
  374. package/dist/hooks/chunk-UT7YC3N2.js +0 -56
@@ -0,0 +1,99 @@
1
+ import { api_outage } from '#scripts/agent/api-outage'
2
+ import { issue_closing_pr } from '#scripts/issue/issue-closing-pr'
3
+ import { issue_merged } from '#scripts/issue/issue-merged'
4
+ import type { issue_state_cli } from '#scripts/issue/issue-state-cli'
5
+ import { run_event_scope } from '#scripts/run/event/run-event-scope'
6
+ import { run_event_stream } from '#scripts/run/event/run-event-stream'
7
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
8
+ import { run_ending } from '#scripts/run/run-ending'
9
+ import { run_merge, type ChildOutcome, type EndingSignals } from './run-merge'
10
+ import { run_merge_ship } from './run-merge-ship'
11
+ import { run_merge_steps, type MergeContext } from './run-merge-steps'
12
+
13
+ // What `run:merge` reads beside a returned child's GitHub state before classifying it — the exit
14
+ // record, the lane's cut, a merged closing pull request and a person's withdrawal — and the
15
+ // classification those readings and the state decide (`run_merge.classify_child`).
16
+
17
+ type IssueRead = Awaited<ReturnType<typeof issue_state_cli.read_issue>>
18
+
19
+ // Whether the child's session ended through its environment — read only when `--output` named the
20
+ // transcript, off otherwise: its exit record shows it could not reach the API, or the session was
21
+ // killed inside a turn. The path is validated inside `run_ending`, so a missing output is "no".
22
+ function read_is_outage(output: string | undefined): boolean {
23
+ if (output === undefined) return false
24
+
25
+ return api_outage.is_outage(run_ending.read_exit(output)) || run_ending.ended_mid_turn(output)
26
+ }
27
+
28
+ async function child_outages(child: string): Promise<number> {
29
+ const events = await run_event_stream_emit.current_events()
30
+
31
+ return events.filter(
32
+ (event) =>
33
+ event.kind === run_event_stream.EVENT_KIND.OUTAGE &&
34
+ run_event_scope.issue_named(event) === child,
35
+ ).length
36
+ }
37
+
38
+ // An outage child is re-dispatched only so many times: one that ends the same way on every dispatch is
39
+ // parked, since merges in other lanes reset the run-wide outage streak and would never stop it. **An
40
+ // unanswered ship stop is told apart from an API outage**: the child reached the API, so it is
41
+ // re-dispatched without adding to the run-wide streak (joshuafolkken/kit#3974).
42
+ async function read_retryable(
43
+ child: string,
44
+ output: string | undefined,
45
+ ): Promise<{ is_outage: boolean; is_unanswered: boolean }> {
46
+ const is_api_outage = read_is_outage(output)
47
+ const is_unanswered = !is_api_outage && (await run_merge_ship.is_unanswered(child, output))
48
+
49
+ if (!is_api_outage && !is_unanswered) return { is_outage: false, is_unanswered: false }
50
+
51
+ const is_outage = (await child_outages(child)) < run_merge.CONSECUTIVE_OUTAGE_LIMIT
52
+
53
+ return { is_outage, is_unanswered: is_outage && is_unanswered }
54
+ }
55
+
56
+ // The timeline is read only for an OPEN, unparked child — the one classification a merged closing pull
57
+ // request changes. A CLOSED child was closed by GitHub, so it must carry no `merged_pr` for
58
+ // `do_merged` to close again.
59
+ async function read_merged_pr(child: string, read: IssueRead): Promise<string | undefined> {
60
+ if (read.kind !== 'state' || run_merge.classify_child(read.state) !== 'failed') return undefined
61
+
62
+ return await issue_closing_pr.read_closing_pr(child)
63
+ }
64
+
65
+ // A child that merged before `lane:withdraw` stopped it is merged, not withdrawn: the withdrawal's
66
+ // event is already written, so only the timeline tells its merged close from the not-planned one. An
67
+ // unread timeline answers `undefined`, and `outcome_of` asks again rather than guessing.
68
+ async function read_is_withdrawn(child: string, read: IssueRead): Promise<boolean | undefined> {
69
+ const events = await run_event_stream_emit.current_events()
70
+
71
+ if (!run_event_scope.is_withdrawn(events, child)) return false
72
+ if (read.kind !== 'state' || run_merge.classify_child(read.state) !== 'merged') return true
73
+ const is_merged = await issue_merged.read_merge_state(child)
74
+
75
+ return is_merged === undefined ? undefined : !is_merged
76
+ }
77
+
78
+ async function read_signals(ctx: MergeContext, read: IssueRead): Promise<EndingSignals> {
79
+ const [is_cut, merged_pr, retryable, is_withdrawn] = await Promise.all([
80
+ run_merge_steps.has_resumable_cut(ctx.child),
81
+ read_merged_pr(ctx.child, read),
82
+ read_retryable(ctx.child, ctx.output),
83
+ read_is_withdrawn(ctx.child, read),
84
+ ])
85
+
86
+ return { ...retryable, is_cut, merged_pr, is_withdrawn }
87
+ }
88
+
89
+ // An unread state is `unresolved` unless a withdrawal already settled the child, which is read locally;
90
+ // a withdrawn child whose timeline could not tell its merge (`is_withdrawn` unset) is `unresolved` too.
91
+ function outcome_of(read: IssueRead, signals: EndingSignals): ChildOutcome {
92
+ if (signals.is_withdrawn === undefined) return 'unresolved'
93
+
94
+ return run_merge.classify_child(read.kind === 'state' ? read.state : undefined, signals)
95
+ }
96
+
97
+ const run_merge_signals = { outcome_of, read_merged_pr, read_signals }
98
+
99
+ export { run_merge_signals }
@@ -73,6 +73,8 @@ interface MergeContext {
73
73
  // The merged pull request that closes the child, when GitHub left the child OPEN after the merge —
74
74
  // the one `merged` child whose issue this command still has to close.
75
75
  merged_pr?: string | undefined
76
+ // The outage is a ship stop no relaunch answered: re-dispatched, but not added to the streak.
77
+ is_unanswered?: boolean | undefined
76
78
  }
77
79
 
78
80
  interface FailedResult {
@@ -359,8 +361,16 @@ interface OutageResult {
359
361
  // the child, so it is re-dispatchable in the same run. Returns the record so the caller can read the
360
362
  // outage streak against its guard. Returns `is_refused: true` without touching labels when the carry
361
363
  // record rejected the count.
364
+ async function count_outage(ctx: MergeContext): Promise<ApplyCarryResult> {
365
+ if (ctx.is_unanswered !== true) return await apply_carry(ctx, run_merge.change_of('outage'))
366
+
367
+ const refused = await refused_carry(ctx)
368
+
369
+ return refused === undefined ? { kind: 'none' } : { kind: 'refused', carry: refused }
370
+ }
371
+
362
372
  async function do_outage(ctx: MergeContext): Promise<OutageResult> {
363
- const result = await apply_carry(ctx, run_merge.change_of('outage'))
373
+ const result = await count_outage(ctx)
364
374
 
365
375
  if (result.kind === 'refused') return { carry: result.carry, is_refused: true }
366
376
 
@@ -23,10 +23,12 @@ const CLOSED_STATE = 'CLOSED'
23
23
  // resets the streak (`run-carry.ts` → `next_failures`), so the guard trips only on an environment
24
24
  // failing every child it is handed rather than on failures scattered across a long run.
25
25
  const CONSECUTIVE_FAILURE_LIMIT = 3
26
- // Three API outages in a row stop the run too — the same shape as the failure
27
- // guard, over its own streak. An outage is a child that could not reach the API, so a run of them is
28
- // the environment being down; stopping is what keeps the run from re-dispatching into a dead API
29
- // forever. A merge or a genuine child failure resets it (`run-carry.ts` → `next_outages`).
26
+ // Three outages in a row stop the run too — the same shape as the failure
27
+ // guard, over its own streak. An outage is a child its environment ended — it could not reach the API
28
+ // or its session was killed inside a turn — so a run of them is the environment being down; stopping is
29
+ // what keeps the run from re-dispatching into a dead API forever. A ship that stopped with no relaunch
30
+ // is re-dispatched too but not counted: its child reached the API (joshuafolkken/kit#3974). A merge or
31
+ // a genuine child failure resets it (`run-carry.ts` → `next_outages`).
30
32
  const CONSECUTIVE_OUTAGE_LIMIT = 3
31
33
  const ONE = 1
32
34
 
@@ -44,9 +46,12 @@ const ONE = 1
44
46
  // the child ended its session on purpose, so it is resumed rather than parked and
45
47
  // counts nothing.
46
48
  // - `failed` — OPEN and carrying neither, and not an outage; the child did not finish.
49
+ // - `withdrawn` — a person withdrew it with `lane:withdraw`, read from the event stream before the
50
+ // GitHub state: closed as not planned, or about to be, it is neither merged nor failed.
47
51
  // - `unresolved` — the state could not be read; re-read before deciding.
48
52
  type ChildOutcome =
49
53
  | 'merged'
54
+ | 'withdrawn'
50
55
  | 'human-review'
51
56
  | 'parked'
52
57
  | 'split'
@@ -64,6 +69,10 @@ interface EndingSignals {
64
69
  // The merged pull request whose `closes #N` names the child, when GitHub left the child OPEN after
65
70
  // merging it.
66
71
  merged_pr?: string | undefined
72
+ // The newest launch of the child was withdrawn by a person (`run_event_scope.is_withdrawn`).
73
+ is_withdrawn?: boolean | undefined
74
+ // The outage is a ship stop no relaunch answered, not an API the child could not reach.
75
+ is_unanswered?: boolean | undefined
67
76
  }
68
77
 
69
78
  const NO_SIGNALS: EndingSignals = { is_outage: false, is_cut: false }
@@ -106,12 +115,16 @@ function open_outcome(state: IssueState, signals: EndingSignals): ChildOutcome {
106
115
  return unfinished_outcome(signals)
107
116
  }
108
117
 
109
- // The signals are read by the CLI and matter only for an OPEN child — a CLOSED child is merged
110
- // regardless of how the process exited.
118
+ // The signals are read by the CLI and, but for a withdrawal, matter only for an OPEN child — a CLOSED
119
+ // child is merged regardless of how the process exited. **A withdrawal is read first**: `lane:withdraw`
120
+ // records it before stopping the child, so the parent may collect it while the issue is still OPEN, and
121
+ // once it is CLOSED as not planned the closed state would otherwise read as a merge.
111
122
  function classify_child(
112
123
  state: IssueState | undefined,
113
124
  signals: EndingSignals = NO_SIGNALS,
114
125
  ): ChildOutcome {
126
+ if (signals.is_withdrawn === true) return 'withdrawn'
127
+
115
128
  if (state === undefined) return 'unresolved'
116
129
 
117
130
  if (is_closed(state)) return 'merged'
@@ -120,9 +133,9 @@ function classify_child(
120
133
  }
121
134
 
122
135
  // The counter change a child's outcome implies. A merge counts a merge (which resets the failure
123
- // streak); a failure counts a failure; a split, parked or human-review child counts nothing — a decision
124
- // waiting on a person is not the environment failing — and `undefined` says the carry record is left
125
- // untouched.
136
+ // streak); a failure counts a failure; a split, parked, withdrawn or human-review child counts nothing —
137
+ // a decision a person made or still owes is not the environment failing — and `undefined` says the
138
+ // carry record is left untouched.
126
139
  function change_of(outcome: ChildOutcome): CarryChange | undefined {
127
140
  if (outcome === 'merged') return { merged: ONE }
128
141
 
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'node:fs'
1
+ import { readFileSync, statSync } from 'node:fs'
2
2
  import { agent_exit_record } from '#scripts/agent/agent-exit-record'
3
3
  import { api_outage } from '#scripts/agent/api-outage'
4
4
  import type { ClaudeResultEvent } from '#scripts/agent/claude-result-event'
@@ -179,7 +179,7 @@ async function read_cut_taken(issue: string): Promise<boolean> {
179
179
  return read.kind === 'carried' && read.cut.invocation === run_cut.invocation_for(issue)
180
180
  }
181
181
 
182
- function read_exit(output_path: string): ClaudeResultEvent | undefined {
182
+ function read_transcript(output_path: string): string | undefined {
183
183
  const safe_path = run_liveness.to_safe_path(output_path)
184
184
 
185
185
  if (safe_path === undefined) return undefined
@@ -188,12 +188,38 @@ function read_exit(output_path: string): ClaudeResultEvent | undefined {
188
188
  // Validated by `run_liveness.to_safe_path` — normalized, required absolute, confined to the
189
189
  // allowed roots — the same check `josh lane:output` gates a transcript path with, reused rather
190
190
  // than re-spelled so the two cannot disagree.
191
- return agent_exit_record.read_exit_record(readFileSync(safe_path, 'utf8')) // NOSONAR
191
+ return readFileSync(safe_path, 'utf8') // NOSONAR
192
+ } catch {
193
+ return undefined
194
+ }
195
+ }
196
+
197
+ function read_exit(output_path: string): ClaudeResultEvent | undefined {
198
+ const transcript = read_transcript(output_path)
199
+
200
+ return transcript === undefined ? undefined : agent_exit_record.read_exit_record(transcript)
201
+ }
202
+
203
+ // When the child's transcript was last written, in epoch milliseconds — `undefined` when unreadable.
204
+ function written_at(output_path: string): number | undefined {
205
+ const safe_path = run_liveness.to_safe_path(output_path)
206
+
207
+ if (safe_path === undefined) return undefined
208
+
209
+ try {
210
+ return statSync(safe_path).mtimeMs // NOSONAR — validated as `read_transcript` validates it
192
211
  } catch {
193
212
  return undefined
194
213
  }
195
214
  }
196
215
 
216
+ // The child's session was killed inside a turn rather than finishing it.
217
+ function ended_mid_turn(output_path: string): boolean {
218
+ const transcript = read_transcript(output_path)
219
+
220
+ return transcript !== undefined && agent_exit_record.is_mid_turn(transcript)
221
+ }
222
+
197
223
  async function read_traces(request: EndingRequest): Promise<EndingTraces> {
198
224
  const [is_child_closed, is_cut_taken, is_tree_dirty] = await Promise.all([
199
225
  read_child_closed(request.issue, request.repo),
@@ -218,7 +244,9 @@ async function check(request: EndingRequest): Promise<EndingDecision> {
218
244
  const run_ending = {
219
245
  check,
220
246
  decide,
247
+ ended_mid_turn,
221
248
  read_exit,
249
+ written_at,
222
250
  }
223
251
 
224
252
  export type { EndingDecision, EndingRequest, EndingTraces, EndingVerdict }
@@ -103,7 +103,7 @@ const REASONS: Record<LivenessVerdict, string> = {
103
103
  'A trace could not be read, so this says nothing about the unit. A trace that could not be read answers `undetermined`, never `stopped`.',
104
104
  }
105
105
 
106
- const ALIVE_ADVICE = 'Keep polling; do not touch the child.'
106
+ const ALIVE_ADVICE = 'Keep waiting on `pnpm josh lane:await`; do not touch the child.'
107
107
  const SETTLED_ADVICE =
108
108
  'Read the child with `pnpm josh issue:state <N>` and take the branch its state says.'
109
109
  // The unit stopped, but *why* it stopped — abandoned mid-implementation, or dead because it could not
@@ -53,16 +53,11 @@ function current_state(): ParentCutState {
53
53
  const THRESHOLD_TEXT = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`
54
54
 
55
55
  const PARENT_CUT_REASON =
56
- '⛔ backlogrun parent hand-off: this session drives a `backlogrun` (it owns the live carry record) and ' +
57
- `its recent-context cost has crossed the shared ${THRESHOLD_TEXT} threshold between merges, so every ` +
58
- 'later request re-reads the accumulated context (joshuafolkken/kit#2947). Take the hand-off now rather ' +
59
- 'than at the next merge: follow `.claude/skills/workflow-commands/backlogrun-progress.md` → "The ' +
60
- 'hand-off" from its `over` branch — open no new lane and take no new child, read `pnpm josh lane:list`, ' +
61
- 'make sure every lane in flight records an output path, then run `pnpm josh run:carry --cut --owner ' +
62
- '"$PPID"` and, unless it answers `capped`, `pnpm josh run:wake`, post the progress comment and end the ' +
63
- 'turn. A lane nobody can poll means the cut does not happen; carry on and this asks again later. ' +
64
- 'Reissue this call when the hand-off is done or cannot be taken — the guard stays quiet for a while ' +
65
- 'after it refuses, so the hand-off calls themselves are not refused.'
56
+ '⛔ backlogrun parent hand-off: this session drives a `backlogrun` and its recent-context cost ' +
57
+ `crossed the ${THRESHOLD_TEXT} threshold between merges. Take the hand-off now: open no new lane, ` +
58
+ 'follow `backlogrun-progress.md` → "The hand-off" (`pnpm josh run:carry --cut --owner "$PPID"`, then ' +
59
+ '`pnpm josh run:wake` unless `capped`) and end the turn. Reissue this call once done or if it ' +
60
+ 'cannot be taken.'
66
61
 
67
62
  // Neither an uncapped parent's own record nor a measured `over` is optional: a capped record has no cut
68
63
  // left to take (`run:carry --cut` answers `capped`), and an unmeasurable session is left alone here —
@@ -0,0 +1,157 @@
1
+ import { hook_decision } from '#scripts/josh/hook-decision'
2
+ import { error_text } from '#scripts/lib/error-message'
3
+ import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
4
+ import { time_shell } from '#scripts/time-runtime/time-shell'
5
+ import { time_spans, type Span } from '#scripts/time-runtime/time-spans'
6
+ import { run_headless } from './run-headless'
7
+ import { run_watcher_hook } from './run-watcher-hook'
8
+
9
+ // The `backlogrun` parent's and the wake session's poll, refused at its first repeat.
10
+ //
11
+ // **The parent waits on a blocking command, never on a re-read.** `backlog:drive` and `lane:await` hold
12
+ // the turn until something the model has to read happens; a state read issued again with nothing
13
+ // decided in between is a poll, and each one is a model turn spent on an answer that has not changed.
14
+ //
15
+ // **"Nothing decided in between" is read off the transcript**: the last completed call before this one
16
+ // ran the same state read on the same operands, with no person's prompt after it. A different command, a person's prompt or
17
+ // any other tool call in between breaks the streak, so a progress question answered twice is not one.
18
+ //
19
+ // **The parent test is `run_headless.driving_carry`'s**, the one the parent hand-off guard reads, so a
20
+ // lane child, a person's session and a handed-off record are left alone. A wake session adopts the
21
+ // record, so it reads as the parent here.
22
+
23
+ const SWITCH_ENV_KEY = 'JOSH_POLL_GUARD'
24
+ const BASH_TOOL = 'Bash'
25
+ const STATE_READS: ReadonlySet<string> = new Set([
26
+ 'josh lane:list',
27
+ 'josh lane:output',
28
+ 'josh run:board',
29
+ 'josh run:carry',
30
+ 'josh run:liveness',
31
+ 'josh run:progress',
32
+ 'josh run:wake',
33
+ ])
34
+
35
+ const POLL_REASON =
36
+ '⛔ poll: this `backlogrun` session repeats a state read with nothing decided in between, on every ' +
37
+ 'occurrence. Wait on `pnpm josh backlog:drive`, or `pnpm josh lane:await <N...> --owner "$PPID"` in ' +
38
+ 'the background, and end the turn; its exit is the wake.'
39
+
40
+ // The flags that make a state-read command write: `run:carry --filed` twice is two filings, and
41
+ // `run:carry --cut` after `run:carry --json` is the hand-off, so neither is a re-read.
42
+ const WRITING_FLAGS: ReadonlySet<string> = new Set([
43
+ '--begin',
44
+ '--cut',
45
+ '--done',
46
+ '--end',
47
+ '--every',
48
+ '--filed',
49
+ '--loop',
50
+ '--mark',
51
+ '--merged',
52
+ '--resume',
53
+ '--retrospective',
54
+ '--start',
55
+ '--stop',
56
+ '--wait',
57
+ ])
58
+
59
+ const FLAG_PREFIX = '-'
60
+ const NAME_WORDS = 2
61
+ const SIGNATURE_SEPARATOR = ' '
62
+
63
+ // A state read keyed by the command and its operands, or `undefined` for a call that is none.
64
+ // **Operands, not flags**: `lane:output 1 <path>` then `lane:output 2 <path>` read two children, while
65
+ // `run:board` and `run:board --chat` print one answer.
66
+ function read_key(signature: string): string | undefined {
67
+ const words = signature.split(SIGNATURE_SEPARATOR)
68
+ const name = words.slice(0, NAME_WORDS).join(SIGNATURE_SEPARATOR)
69
+
70
+ if (!STATE_READS.has(name) || words.some((word) => WRITING_FLAGS.has(word))) return undefined
71
+
72
+ return words.filter((word) => !word.startsWith(FLAG_PREFIX)).join(SIGNATURE_SEPARATOR)
73
+ }
74
+
75
+ function read_keys(signatures: ReadonlyArray<string>): Array<string> {
76
+ return signatures.map((signature) => read_key(signature)).filter((key) => key !== undefined)
77
+ }
78
+
79
+ function state_reads_of(command: string): ReadonlyArray<string> {
80
+ return read_keys(time_shell.josh_signatures_of(command))
81
+ }
82
+
83
+ function command_of(input: unknown): string {
84
+ if (typeof input !== 'object' || input === null || !('command' in input)) return ''
85
+
86
+ return typeof input.command === 'string' ? input.command : ''
87
+ }
88
+
89
+ // The state reads the call runs, or none for a call this guard never judges: not a shell call, or a
90
+ // delegated unit's (it names the parent's transcript, so only `agent_id` tells it apart).
91
+ function candidate_reads(raw_payload: string): ReadonlyArray<string> {
92
+ const payload = hook_decision.parse_hook_payload(raw_payload)
93
+
94
+ if (payload?.tool_name !== BASH_TOOL || (payload.agent_id ?? '') !== '') return []
95
+
96
+ return state_reads_of(command_of(payload.tool_input))
97
+ }
98
+
99
+ // The last completed call or person's prompt; a model span is the turn itself, never a decision.
100
+ function last_step(tail: string): Span | undefined {
101
+ return time_spans
102
+ .parse_timeline(tail)
103
+ .spans.findLast((span) => span.category !== time_spans.MODEL_CATEGORY)
104
+ }
105
+
106
+ function repeats_last_call(tail: string, reads: ReadonlyArray<string>): boolean {
107
+ const last = last_step(tail)
108
+
109
+ if (last?.category !== time_spans.TOOL_CATEGORY) return false
110
+
111
+ const last_reads = read_keys(last.josh_signatures)
112
+
113
+ return reads.some((key) => last_reads.includes(key))
114
+ }
115
+
116
+ // Every cheap test first: the carry record is read only for a call that already repeats a state read.
117
+ function is_repeat(raw_payload: string): boolean {
118
+ const reads = hook_decision.is_switch_enabled(SWITCH_ENV_KEY) ? candidate_reads(raw_payload) : []
119
+ const transcript = reads.length === 0 ? undefined : run_watcher_hook.transcript_of(raw_payload)
120
+
121
+ if (transcript === undefined) return false
122
+
123
+ return repeats_last_call(time_density_hook.read_tail(transcript), reads)
124
+ }
125
+
126
+ /**
127
+ * The deny reason for a state read the driving `backlogrun` parent (or its wake session) issues again
128
+ * with nothing decided since the last one, or `undefined` when the call proceeds.
129
+ */
130
+ async function poll_reason(
131
+ raw_payload: string,
132
+ is_parent: () => Promise<boolean> = run_headless.is_backlog_parent,
133
+ ): Promise<string | undefined> {
134
+ if (!is_repeat(raw_payload)) return undefined
135
+
136
+ return (await is_parent()) ? POLL_REASON : undefined
137
+ }
138
+
139
+ // It fails open: an unreadable transcript or carry record allows the call rather than wedging the run.
140
+ async function safe_poll_reason(raw_payload: string): Promise<string | undefined> {
141
+ try {
142
+ return await poll_reason(raw_payload)
143
+ } catch (error) {
144
+ error_text.trace_swallowed('run_poll_hook.poll_reason', error)
145
+
146
+ return undefined
147
+ }
148
+ }
149
+
150
+ const run_poll_hook = {
151
+ POLL_REASON,
152
+ SWITCH_ENV_KEY,
153
+ poll_reason,
154
+ safe_poll_reason,
155
+ }
156
+
157
+ export { run_poll_hook }
@@ -343,8 +343,7 @@ async function read_candidate(
343
343
  ): Promise<ChildState | undefined> {
344
344
  const state = await read_pr_of_branch(name)
345
345
 
346
- if (state.pr_state !== NO_PR) return state
347
- if (await has_commits_beyond(context.base, references)) return state
346
+ if (state.pr_state !== NO_PR || (await has_commits_beyond(context.base, references))) return state
348
347
 
349
348
  return (await is_held_dirty(name, context.lane)) ? state : undefined
350
349
  }
@@ -14,15 +14,16 @@ import { run_prep, type PrepParts } from './run-prep'
14
14
  import { run_prep_cli } from './run-prep-cli'
15
15
  import { run_retrospective } from './run-retrospective'
16
16
  import { run_step, type ParentInput, type StepInput } from './run-step'
17
+ import { run_step_section } from './run-step-section'
17
18
 
18
- // `josh run:step [<N>]` — print the run's next single action, computed from the event stream, the carry
19
+ // `josh run:step [<N>]` — print the run's next single action, followed by the section that governs it
20
+ // (`run-step-section.ts`), computed from the event stream, the carry
19
21
  // record and the issue state. It reads exactly those three, never the
20
22
  // conversation: the issue facts through `run:prep`'s own gather (so this and `run:next` never answer
21
23
  // from different reads), the newest event off the stream, and the carry record's kind. The reader runs
22
24
  // the printed line, returns the result through `run:event`, and asks again — the loop `run:step`
23
25
  // exists to close.
24
26
 
25
- const SUCCESS_EXIT_CODE = 0
26
27
  const FAILURE_EXIT_CODE = 1
27
28
  const ARGV_OFFSET = 2
28
29
  const USAGE =
@@ -154,16 +155,31 @@ function parent_input(directory: string | undefined): ParentInput {
154
155
  // `unknown` is the one answer with nothing to run — no record this session drives — so it exits non-zero
155
156
  // with the usage, exactly as a position missing its issue state does.
156
157
  async function run_parent(): Promise<number> {
157
- const { line } = run_step.parent_action(parent_input(await run_carry.repository_directory()))
158
+ const action = run_step.parent_action(parent_input(await run_carry.repository_directory()))
158
159
 
159
- console.info(line)
160
- if (line !== run_step.UNKNOWN) return SUCCESS_EXIT_CODE
160
+ console.info(action.line)
161
+ if (action.line !== run_step.UNKNOWN) return run_step_section.print_for(action)
161
162
 
162
163
  console.error(USAGE)
163
164
 
164
165
  return FAILURE_EXIT_CODE
165
166
  }
166
167
 
168
+ function report({ input, ship_problems }: Gathered): number {
169
+ const action = run_step.next_action(input)
170
+
171
+ console.info(action.line)
172
+ // On stderr, so stdout stays the action line and its rule: what `josh ship` would
173
+ // refuse on is said at every step, while it is still cheap to meet.
174
+ if (ship_problems.length > 0) console.error(run_prep.ship_body(ship_problems))
175
+
176
+ // A state that could not be read exits non-zero, exactly as `run:prep` and `run:next` exit on the
177
+ // same failure, so a position missing its issue state is never read as a confident next step.
178
+ if (input.state === undefined) return FAILURE_EXIT_CODE
179
+
180
+ return run_step_section.print_for(action)
181
+ }
182
+
167
183
  async function run(argv: ReadonlyArray<string>): Promise<number> {
168
184
  if (argv.length === 0) return await run_parent()
169
185
 
@@ -175,16 +191,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
175
191
  return FAILURE_EXIT_CODE
176
192
  }
177
193
 
178
- const { input, ship_problems } = await gather(issue_number)
179
-
180
- console.info(run_step.next_action(input).line)
181
- // On stderr, so stdout stays the one action line: what `josh ship` would
182
- // refuse on is said at every step, while it is still cheap to meet.
183
- if (ship_problems.length > 0) console.error(run_prep.ship_body(ship_problems))
184
-
185
- // A state that could not be read exits non-zero, exactly as `run:prep` and `run:next` exit on the
186
- // same failure, so a position missing its issue state is never read as a confident next step.
187
- return input.state === undefined ? FAILURE_EXIT_CODE : SUCCESS_EXIT_CODE
194
+ return report(await gather(issue_number))
188
195
  }
189
196
 
190
197
  async function main(argv: ReadonlyArray<string>): Promise<void> {
@@ -0,0 +1,85 @@
1
+ import { document_section_cli } from '#scripts/document/document-section-cli'
2
+ import { run_step, type StepAction } from './run-step'
3
+
4
+ // The section that governs each action `run:step` and `run:entry` print — the one table the action →
5
+ // rule mapping lives in, so the manifests no longer carry a route table the reader walks to find it.
6
+ // The command prints the section after its action line, which saves the `doc:section` turn the reader
7
+ // would otherwise spend fetching a section the action already determined.
8
+ //
9
+ // **A heading that does not resolve fails the command**, exactly as `doc:section` refuses it: a pointer
10
+ // renamed away would otherwise print the action without its rule, and the reader would act unread.
11
+
12
+ const JOSH_PREFIX = 'pnpm josh '
13
+ const COMMAND_SEPARATOR = ' '
14
+ const SUCCESS_EXIT_CODE = 0
15
+
16
+ // The key `run:entry` prints under — not a `run:step` action, but the entry's own read, kept in the
17
+ // same table so every mechanically-chosen section has one home.
18
+ const ENTRY = 'entry'
19
+
20
+ interface SectionReference {
21
+ document: string
22
+ heading: string
23
+ }
24
+
25
+ function reference(document: string, heading: string): SectionReference {
26
+ return { document, heading }
27
+ }
28
+
29
+ // The gate's chain governs both the implementation it follows and a stopped ship's report.
30
+ const CHAIN = reference('chain-rule.md', 'Run the review-to-merge chain')
31
+ const PROGRESS = 'backlogrun-progress.md'
32
+
33
+ // Keyed on a verdict token, or on the josh subcommand a command action runs. An action with no row —
34
+ // `wait`, `stop`, a `decide:` line — has no rule to read beyond its own line.
35
+ const GOVERNING_SECTIONS: ReadonlyMap<string, SectionReference> = new Map([
36
+ [ENTRY, reference('split-assessment.md', 'The question')],
37
+ [run_step.IMPLEMENT, CHAIN],
38
+ [run_step.HUMAN_REVIEW, reference('needs-human-review.md', '`needs-human-review`')],
39
+ [run_step.UPDATE_DEPS, reference('latest-gate.md', 'The dependency update')],
40
+ ['followup', reference('followup.md', 'Run `pnpm josh followup`')],
41
+ ['review:round2', reference('prompts/review.md', 'Review round cap')],
42
+ ['run:merge', reference(PROGRESS, "Running a named epic's children")],
43
+ ['backlog:next', reference('backlogrun-steps.md', 'The loop')],
44
+ ['run:cut', reference('pre-gate-cut.md', 'Resuming')],
45
+ ['ship', CHAIN],
46
+ ['retrospective', reference('retrospective.md', 'The end-of-run retrospective')],
47
+ ['run:carry', reference(PROGRESS, 'The hand-off')],
48
+ ])
49
+
50
+ function key_of(action: StepAction): string {
51
+ if (action.kind !== 'command') return action.line
52
+
53
+ const [subcommand = ''] = action.line.slice(JOSH_PREFIX.length).split(COMMAND_SEPARATOR)
54
+
55
+ return subcommand
56
+ }
57
+
58
+ function section_for(key: string): SectionReference | undefined {
59
+ return GOVERNING_SECTIONS.get(key)
60
+ }
61
+
62
+ // Printed through `doc:section` itself, so the extent, the Bash-cap split and the refusal of an
63
+ // unresolved heading are that command's, never a second copy of them.
64
+ function print_section(key: string, root: string = process.cwd()): number {
65
+ const found = section_for(key)
66
+
67
+ if (found === undefined) return SUCCESS_EXIT_CODE
68
+
69
+ return document_section_cli.run([found.document, found.heading], root)
70
+ }
71
+
72
+ function print_for(action: StepAction, root: string = process.cwd()): number {
73
+ return print_section(key_of(action), root)
74
+ }
75
+
76
+ const run_step_section = {
77
+ ENTRY,
78
+ GOVERNING_SECTIONS,
79
+ key_of,
80
+ print_for,
81
+ print_section,
82
+ }
83
+
84
+ export type { SectionReference }
85
+ export { run_step_section }
@@ -27,8 +27,7 @@ const STOP_RECOVERY =
27
27
  // this type is for the run that halted needing a person. An `unreadable` record is not announced: with
28
28
  // nothing readable there is no run this can honestly say stopped.
29
29
  function plan(read: CarryRead, reason: string | undefined): StopNotice | undefined {
30
- if (reason === undefined || reason.length === 0) return undefined
31
- if (read.kind !== 'carried') return undefined
30
+ if (reason === undefined || reason.length === 0 || read.kind !== 'carried') return undefined
32
31
 
33
32
  return { reason }
34
33
  }
@@ -49,9 +49,12 @@ async function owes_watcher(): Promise<boolean> {
49
49
  // `run_progress_clock.life_target_of(git_directory)`. Passed in rather than resolved here so the
50
50
  // caller can use whichever resolution fits its sync/async context.
51
51
  async function check(life_target: string): Promise<GuardResult> {
52
- if (!(await owes_watcher())) return OK_RESULT
53
-
54
- if (run_progress_clock.is_life_fresh(life_target, STALE_THRESHOLD_MS)) return OK_RESULT
52
+ if (
53
+ !(await owes_watcher()) ||
54
+ run_progress_clock.is_life_fresh(life_target, STALE_THRESHOLD_MS)
55
+ ) {
56
+ return OK_RESULT
57
+ }
55
58
 
56
59
  return { kind: 'stale', note: STALE_NOTE }
57
60
  }