@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
@@ -17,11 +17,17 @@ import { run_board_setup, type SetupRead } from './run-board-setup'
17
17
  // statuses are laid over is the slow GitHub half, and so is what a child that left the open listing
18
18
  // answers when read closed.
19
19
 
20
- type ItemState = 'running' | 'merged' | 'parked' | 'done' | 'waiting' | 'human' | 'stopped'
20
+ type ItemState =
21
+ 'running' | 'merged' | 'parked' | 'done' | 'waiting' | 'human' | 'stopped' | 'withdrawn'
21
22
 
22
23
  interface ItemStatus {
23
24
  state: ItemState
25
+ // When the child first launched in this run — a re-dispatch or a restart does not move it, so the
26
+ // row's time is the lane's whole time.
24
27
  started_ms?: number | undefined
28
+ // The child's newest launch, present only where it launched again after `started_ms` — what the
29
+ // GitHub read is weighed against.
30
+ launched_ms?: number | undefined
25
31
  ended_ms?: number | undefined
26
32
  // The seat-free lane label (`lane <N>`) of a running child, when it holds one — the running test reads
27
33
  // it; the board does not draw it.
@@ -61,22 +67,40 @@ const ENDING_STATES: Readonly<Partial<Record<string, ItemState>>> = {
61
67
  [KIND.PARK]: 'parked',
62
68
  [KIND.SPLIT]: 'done',
63
69
  [KIND.STOP]: 'done',
70
+ [KIND.WITHDRAW]: 'withdrawn',
64
71
  }
65
72
  // A cut or a resume says the child is still running, on whatever phase it had reached.
66
73
  const CONTINUING_KINDS: ReadonlySet<string> = new Set([KIND.CUT, KIND.RESUME])
67
- // A person's restart starts the child over in a fresh session, so its track starts over too.
74
+ // A person's restart, and a re-dispatch after an outage, start the child over in a fresh session, so
75
+ // its track starts over too; its time does not (`launched`).
68
76
  const LAUNCH_KINDS: ReadonlySet<string> = new Set([KIND.CHILD_LAUNCH, KIND.RESTART])
77
+ // What the GitHub read may still settle: a running child, and one whose lane has no child left in it —
78
+ // a child that merged and then died is drawn merged, not stopped.
79
+ const UNSETTLED_STATES: ReadonlySet<ItemState> = new Set(['running', 'stopped'])
80
+ // A parked child waits on a person, who may close its issue after the park; only a closed read settles
81
+ // it again, so one still open stays parked whatever its labels say (joshuafolkken/kit#3958).
82
+ const PARKED_STATE: ItemState = 'parked'
69
83
 
70
84
  function moment(event: RunEvent): number {
71
85
  return Date.parse(event.at)
72
86
  }
73
87
 
74
- function launched(event: RunEvent): ItemStatus {
88
+ // The newest launch carried on, where the child launched more than once.
89
+ function relaunch_of(status: ItemStatus | undefined): Pick<ItemStatus, 'launched_ms'> {
90
+ const launched_ms = status?.launched_ms
91
+
92
+ return launched_ms === undefined ? {} : { launched_ms }
93
+ }
94
+
95
+ // joshuafolkken/kit#3971: a launch keeps the start of the child's first one in this run.
96
+ function launched(previous: ItemStatus | undefined, event: RunEvent): ItemStatus {
75
97
  const at_ms = moment(event)
98
+ const started_ms = previous?.started_ms ?? at_ms
76
99
 
77
100
  return {
78
101
  state: 'running',
79
- started_ms: at_ms,
102
+ started_ms,
103
+ ...(started_ms !== at_ms && { launched_ms: at_ms }),
80
104
  track: [run_board_phase.LAUNCHED_PHASE],
81
105
  phase_ms: at_ms,
82
106
  }
@@ -95,7 +119,12 @@ function moved(previous: ItemStatus, track: ReadonlyArray<Phase>, event: RunEven
95
119
  function settled_as(previous: ItemStatus | undefined, state: ItemState): ItemStatus {
96
120
  const track = previous?.track
97
121
 
98
- return { state, started_ms: previous?.started_ms, ...(track !== undefined && { track }) }
122
+ return {
123
+ state,
124
+ started_ms: previous?.started_ms,
125
+ ...relaunch_of(previous),
126
+ ...(track !== undefined && { track }),
127
+ }
99
128
  }
100
129
 
101
130
  function ended(previous: ItemStatus | undefined, state: ItemState, event: RunEvent): ItemStatus {
@@ -105,7 +134,7 @@ function ended(previous: ItemStatus | undefined, state: ItemState, event: RunEve
105
134
  function continued(previous: ItemStatus | undefined): ItemStatus {
106
135
  const { started_ms, track, phase_ms } = previous ?? {}
107
136
 
108
- return { state: 'running', started_ms, track, phase_ms }
137
+ return { state: 'running', started_ms, ...relaunch_of(previous), track, phase_ms }
109
138
  }
110
139
 
111
140
  // A stage line carrying its attempt redraws the track of any child launched on this stream — a settled
@@ -140,7 +169,7 @@ function advanced(previous: ItemStatus | undefined, event: RunEvent): ItemStatus
140
169
 
141
170
  // One event folded into the status of the child it names; an event the board does not track keeps it.
142
171
  function next_status(previous: ItemStatus | undefined, event: RunEvent): ItemStatus | undefined {
143
- if (LAUNCH_KINDS.has(event.kind)) return launched(event)
172
+ if (LAUNCH_KINDS.has(event.kind)) return launched(previous, event)
144
173
 
145
174
  const ending = ENDING_STATES[event.kind]
146
175
 
@@ -162,8 +191,9 @@ function fold_events(events: ReadonlyArray<RunEvent>): Map<number, ItemStatus> {
162
191
  return statuses
163
192
  }
164
193
 
165
- // A lane is this run's only when its issue is on the run's stream or in the run's plan, so a lane left
166
- // over from an earlier day is not drawn as this run's child.
194
+ // A lane is this run's only when its issue is on the run's stream or in `in_run`, so a lane left over
195
+ // from an earlier day is not drawn as this run's child. `in_run` leaves out a planned issue whose lane
196
+ // an earlier run dispatched (`run-board-github.ts`).
167
197
  function is_running_lane(
168
198
  issue: number,
169
199
  status: ItemStatus | undefined,
@@ -175,13 +205,25 @@ function is_running_lane(
175
205
  return in_run.has(issue) && !run_board_setup.is_lane_pending(events, issue)
176
206
  }
177
207
 
208
+ // A lane no child is working in is stopped whatever the stream last said: a child that died writes no
209
+ // ending, so its row would otherwise run — and its phase time grow — for as long as the lane stays.
210
+ function lane_status(status: ItemStatus | undefined, lane: string, is_idle: boolean): ItemStatus {
211
+ const held = { lane: `lane ${lane}` }
212
+
213
+ return is_idle
214
+ ? { ...settled_as(status, 'stopped'), ...held }
215
+ : { state: 'running', ...status, ...held }
216
+ }
217
+
178
218
  // Every child the run has touched, with the lane each running child holds. A lane the stream never saw
179
219
  // launched — a run restored after the stream rolled — still reads as running while its issue is in the
180
- // run's plan (`in_run`); one the run is still setting up for stays waiting until its launch.
220
+ // run's plan (`in_run`); one the run is still setting up for stays waiting until its launch. A lane in
221
+ // `idle` has no child working in it (`lane-spent.ts`).
181
222
  function statuses_of(
182
223
  events: ReadonlyArray<RunEvent>,
183
224
  lanes: ReadonlyArray<string>,
184
225
  in_run: ReadonlySet<number>,
226
+ idle: ReadonlyArray<string> = [],
185
227
  ): ReadonlyMap<number, ItemStatus> {
186
228
  const statuses = fold_events(events)
187
229
 
@@ -190,7 +232,7 @@ function statuses_of(
190
232
  const status = statuses.get(issue)
191
233
 
192
234
  if (is_running_lane(issue, status, events, in_run)) {
193
- statuses.set(issue, { state: 'running', ...status, lane: `lane ${lane}` })
235
+ statuses.set(issue, lane_status(status, lane, idle.includes(lane)))
194
236
  }
195
237
  }
196
238
 
@@ -212,9 +254,14 @@ function is_listed_closed(issue: number, read: OpenRead): boolean {
212
254
  return read.open_numbers !== undefined && !read.open_numbers.has(issue)
213
255
  }
214
256
 
215
- // The listing was read after the child launched, so what it says about the child is this run's.
257
+ // When the child last launched — what a GitHub read is weighed against.
258
+ function launch_ms(status: ItemStatus): number | undefined {
259
+ return status.launched_ms ?? status.started_ms
260
+ }
261
+
262
+ // The listing was read after the child last launched, so what it says about the child is current.
216
263
  function is_read_after_launch(status: ItemStatus, read: OpenRead): boolean {
217
- return read.read_ms !== undefined && (status.started_ms ?? 0) <= read.read_ms
264
+ return read.read_ms !== undefined && (launch_ms(status) ?? 0) <= read.read_ms
218
265
  }
219
266
 
220
267
  // A child launched before the listing was read and missing from it has closed since.
@@ -242,26 +289,30 @@ function labelled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus
242
289
 
243
290
  // A close dated before the launch is an earlier life of the issue, not how this run's child ended.
244
291
  function is_closed_before_launch(status: ItemStatus, closed: ClosedIssue): boolean {
245
- if (closed.closed_ms === undefined || status.started_ms === undefined) return false
292
+ const launch = launch_ms(status)
293
+
294
+ if (launch === undefined || closed.closed_ms === undefined) return false
295
+
296
+ return closed.closed_ms < launch
297
+ }
298
+
299
+ function closed_state(closed: ClosedIssue): ItemState {
300
+ if (closed.is_merged) return 'merged'
246
301
 
247
- return closed.closed_ms < status.started_ms
302
+ return closed.is_not_planned ? 'withdrawn' : 'done'
248
303
  }
249
304
 
250
- // A running child read closed ends when GitHub says it closed, merged or not.
305
+ // A child read closed ends when GitHub says it closed — merged, withdrawn as not planned, or done.
251
306
  function closed_status(
252
307
  status: ItemStatus,
253
308
  closed: ClosedIssue | undefined,
254
309
  ): ItemStatus | undefined {
255
310
  if (closed === undefined || is_closed_before_launch(status, closed)) return undefined
256
311
 
257
- const state = closed.is_merged ? 'merged' : 'done'
258
-
259
- return { ...settled_as(status, state), ended_ms: closed.closed_ms }
312
+ return { ...settled_as(status, closed_state(closed)), ended_ms: closed.closed_ms }
260
313
  }
261
314
 
262
- function settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
263
- if (status.state !== 'running') return status
264
-
315
+ function unsettled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
265
316
  const from_closed = closed_status(status, read.closed.get(issue))
266
317
 
267
318
  if (from_closed !== undefined) return from_closed
@@ -271,11 +322,26 @@ function settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus
271
322
  return labelled(issue, status, read)
272
323
  }
273
324
 
325
+ // The closed read is kept for the run, so a parked child a person reopened since is told apart by the
326
+ // listing holding it again.
327
+ function parked_settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
328
+ if (read.open_numbers?.has(issue) === true) return status
329
+
330
+ return closed_status(status, read.closed.get(issue)) ?? status
331
+ }
332
+
333
+ function settled(issue: number, status: ItemStatus, read: OpenRead): ItemStatus {
334
+ if (status.state === PARKED_STATE) return parked_settled(issue, status, read)
335
+
336
+ return UNSETTLED_STATES.has(status.state) ? unsettled(issue, status, read) : status
337
+ }
338
+
274
339
  // **The stream stays the source of truth; the listing only unsticks it.** A child whose merge never
275
340
  // reached the stream would read as running forever, so a running child the plan's open listing no
276
341
  // longer holds is drawn finished instead — merged and timed once GitHub was read for it, finished
277
342
  // without a time while that read has not answered. One the listing still holds is drawn as its labels
278
- // say, so a child that stopped with no parent to record it does not read as running.
343
+ // say, so a child that stopped with no parent to record it does not read as running. A parked child is
344
+ // settled again only by a closed read.
279
345
  function settle_closed(
280
346
  statuses: ReadonlyMap<number, ItemStatus>,
281
347
  read: OpenRead,
@@ -17,8 +17,13 @@ type ThanksState = Pick<BoardState, 'thanks' | 'thanks_fetch' | 'thanks_attempte
17
17
  function launch_thanks(state: BoardState, ports: BoardPorts, now_ms: number): BoardState {
18
18
  const { read_thanks } = ports
19
19
 
20
- if (read_thanks === undefined || state.thanks_fetch !== undefined) return state
21
- if (!is_due(state.thanks_attempted_ms, TTL_MS, now_ms)) return state
20
+ if (
21
+ read_thanks === undefined ||
22
+ state.thanks_fetch !== undefined ||
23
+ !is_due(state.thanks_attempted_ms, TTL_MS, now_ms)
24
+ ) {
25
+ return state
26
+ }
22
27
 
23
28
  const thanks_fetch: Fetch<Thanks> = run_board_fetch.launch(read_thanks, state.thanks)
24
29
 
@@ -1,7 +1,9 @@
1
+ import { lane_await } from '#scripts/lane/lane-await'
1
2
  import { run_progress } from '#scripts/run/progress/run-progress'
2
3
  import type { ClosedIssue } from './run-board-closed'
3
4
  import { run_board_github } from './run-board-github'
4
5
  import { run_board_header, type BoardHeader } from './run-board-header'
6
+ import { run_board_idle } from './run-board-idle'
5
7
  import { run_board_labels } from './run-board-labels'
6
8
  import { run_board_layout, type BoardLayout, type BoardPlan } from './run-board-layout'
7
9
  import { run_board_link } from './run-board-link'
@@ -34,10 +36,30 @@ const { FRESH_STATE, is_due } = run_board_state
34
36
  const { PLAN_RETRY_MS, gather_github, statuses_for } = run_board_github
35
37
  const { gather_thanks } = run_board_thanks
36
38
 
37
- async function reread(state: BoardState, ports: BoardPorts, now_ms: number): Promise<BoardState> {
39
+ async function read_at(state: BoardState, ports: BoardPorts, now_ms: number): Promise<BoardState> {
40
+ const local = await ports.read_local()
41
+ const idle_since = run_board_idle.since_of(state.idle_since, local?.idle_lanes ?? [], now_ms)
42
+
43
+ return { ...state, local, read_ms: now_ms, idle_since }
44
+ }
45
+
46
+ // A single frame has no earlier read to confirm an idle lane against (`run-board-idle.ts`).
47
+ function is_unconfirmed(before: BoardState, read: BoardState, pace: Pace): boolean {
48
+ return pace === 'settled' && before.read_ms === undefined && read.idle_since.size > 0
49
+ }
50
+
51
+ async function reread(state: BoardState, ports: BoardPorts, pace: Pace): Promise<BoardState> {
52
+ const now_ms = ports.now()
53
+
38
54
  if (!is_due(state.read_ms, LOCAL_READ_MS, now_ms)) return state
39
55
 
40
- return { ...state, local: await ports.read_local(), read_ms: now_ms }
56
+ const read = await read_at(state, ports, now_ms)
57
+
58
+ if (!is_unconfirmed(state, read, pace)) return read
59
+
60
+ await ports.sleep(lane_await.RECONFIRM_MS)
61
+
62
+ return await read_at(read, ports, ports.now())
41
63
  }
42
64
 
43
65
  // Each lane's usage, read on the machine's second; a reading that failed draws no column.
@@ -60,8 +82,10 @@ async function resample(state: BoardState, ports: BoardPorts, now_ms: number): P
60
82
  const at_ms = ports.now()
61
83
  const machine = { sample: await ports.read_machine(), at_ms }
62
84
  const gauges = run_board_machine.gauges_of(state.machine, machine)
85
+ const budget_waits = ports.read_waits?.() ?? []
86
+ const usages = await read_usages(state, ports)
63
87
 
64
- return { ...state, ...(await read_usages(state, ports)), machine, gauges, sampled_ms: now_ms }
88
+ return { ...state, ...usages, machine, gauges, budget_waits, sampled_ms: now_ms }
65
89
  }
66
90
 
67
91
  // The state of the run on screen; a different run starts its plan state from nothing, so no row, count
@@ -69,9 +93,9 @@ async function resample(state: BoardState, ports: BoardPorts, now_ms: number): P
69
93
  function state_for(state: BoardState, local: LocalRead): BoardState {
70
94
  if (state.run_started_ms === local.started_ms) return state
71
95
 
72
- const { local: kept, read_ms } = state
96
+ const { local: kept, read_ms, idle_since } = state
73
97
 
74
- return { ...FRESH_STATE, local: kept, read_ms, run_started_ms: local.started_ms }
98
+ return { ...FRESH_STATE, local: kept, read_ms, idle_since, run_started_ms: local.started_ms }
75
99
  }
76
100
 
77
101
  // The plan with a closed child's title and labels under the listing's own.
@@ -108,12 +132,13 @@ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): B
108
132
  closed,
109
133
  labels: plan.labels,
110
134
  }
111
- const statuses = run_board_status.settle_closed(statuses_for(plan, local), read)
135
+ const statuses = run_board_status.settle_closed(statuses_for(state, plan, local), read)
112
136
  const reason_of = run_board_reason.reader({
113
137
  plan,
114
138
  statuses,
115
139
  events: local.events,
116
140
  free_lanes: local.free_lanes,
141
+ occupied_lanes: local.occupied_lanes,
117
142
  is_stopped: activity_here(local).is_stopped,
118
143
  })
119
144
 
@@ -125,8 +150,10 @@ function planned_layout(state: BoardState, plan: BoardPlan, local: LocalRead): B
125
150
  function layout_for(state: BoardState, local: LocalRead): BoardLayout {
126
151
  if (state.plan !== undefined) return planned_layout(state, state.plan, local)
127
152
 
153
+ const stopped = run_board_idle.stopped_of(state.idle_since, state.read_ms)
154
+
128
155
  return run_board_layout.local_layout_of(
129
- run_board_status.statuses_of(local.events, local.lanes, new Set()),
156
+ run_board_status.statuses_of(local.events, local.lanes, new Set(), stopped),
130
157
  )
131
158
  }
132
159
 
@@ -168,6 +195,7 @@ function header_of(
168
195
  is_plan_loading: state.plan_fetch !== undefined,
169
196
  machine: state.gauges,
170
197
  usages: state.usages,
198
+ budget_waits: state.budget_waits,
171
199
  form: redraw.ports.form,
172
200
  repo: state.plan?.context.repo,
173
201
  spinner: redraw.ports.is_tty ? SPOT : undefined,
@@ -220,8 +248,9 @@ async function tick(
220
248
  ports: BoardPorts,
221
249
  pace: Pace = 'settled',
222
250
  ): Promise<BoardState> {
251
+ const read = await reread(state, ports, pace)
252
+ // Taken after the read, which a single frame may have waited on to confirm an idle lane.
223
253
  const now_ms = ports.now()
224
- const read = await reread(state, ports, now_ms)
225
254
  const { local } = read
226
255
 
227
256
  if (local === undefined) {
@@ -269,9 +269,7 @@ function to_named_request(values: ParsedValues, owner: CarryOwner): Request | un
269
269
  }
270
270
 
271
271
  function to_request(values: ParsedValues): Request | undefined {
272
- if (group_count(values) > ONE_GROUP) return undefined
273
-
274
- if (!is_summary_paired(values)) return undefined
272
+ if (group_count(values) > ONE_GROUP || !is_summary_paired(values)) return undefined
275
273
 
276
274
  const owner = to_owner(values.owner)
277
275
 
@@ -76,8 +76,13 @@ function is_same_conversation(carry: RunCarry, owner: CarryOwner): boolean {
76
76
  // a new one, so the record goes on naming a live process once the conversation falls quiet. A record a
77
77
  // cut handed off is the successor's to take, never the cutting conversation's to take back.
78
78
  function reclaim_owner(carry: RunCarry, owner: CarryOwner): RunCarry {
79
- if (carry.is_handed_off === true || !is_same_conversation(carry, owner)) return carry
80
- if (owner.pid === carry.owner_pid) return carry
79
+ if (
80
+ carry.is_handed_off === true ||
81
+ !is_same_conversation(carry, owner) ||
82
+ owner.pid === carry.owner_pid
83
+ ) {
84
+ return carry
85
+ }
81
86
 
82
87
  return { ...carry, owner_pid: owner.pid, owner_start: owner.start }
83
88
  }
@@ -3,7 +3,6 @@ import { fileURLToPath } from 'node:url'
3
3
  import { agent_session_role } from '#scripts/agent/agent-session-role'
4
4
  import { cost_cli } from '#scripts/cost-runtime/cost-cli'
5
5
  import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
6
- import { git_command } from '#scripts/git/git-command'
7
6
  import { issue_cite } from '#scripts/issue/issue-cite'
8
7
  import { session_cite } from '#scripts/issue/session-cite'
9
8
  import { lane_registry, type LaneInfo } from '#scripts/lane/lane-registry'
@@ -12,9 +11,11 @@ import { openai_lane_supervisor } from '#scripts/lane/openai-lane-supervisor'
12
11
  import { run_event_stream } from '#scripts/run/event/run-event-stream'
13
12
  import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
14
13
  import { run_cli_fault } from '#scripts/run/run-cli-fault'
15
- import { run_cut, type CutState, type RunCut } from './run-cut'
14
+ import { run_cut, type RunCut } from './run-cut'
16
15
  import { run_cut_args, type Request } from './run-cut-args'
16
+ import { run_cut_decline } from './run-cut-decline'
17
17
  import { run_cut_handoff } from './run-cut-handoff'
18
+ import { run_cut_ready } from './run-cut-ready'
18
19
  import { run_cut_report } from './run-cut-report'
19
20
 
20
21
  // `josh run:cut` — the record that lets a lane child end its process before the gate and a fresh one
@@ -57,8 +58,10 @@ const {
57
58
  report_busy,
58
59
  report_fault,
59
60
  report_handed_off,
61
+ report_held_cut,
60
62
  report_incomplete,
61
63
  report_over,
64
+ report_relaunched_cut,
62
65
  report_reviewer,
63
66
  report_stale,
64
67
  report_under_threshold,
@@ -122,14 +125,11 @@ function relaunch(target: string, lane: LaneInfo, phase: string): number {
122
125
 
123
126
  if (notes.length > 0) console.error(notes.join('\n'))
124
127
 
125
- return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
128
+ return report_relaunched_cut()
126
129
  }
127
130
 
128
- async function is_lane_branch(state: CutState): Promise<boolean> {
129
- const default_branch = await git_command.get_default_branch()
130
-
131
- return state.branch !== default_branch && state.is_dirty
132
- }
131
+ const { is_lane_branch } = run_cut_ready
132
+ const { declined } = run_cut_decline
133
133
 
134
134
  // An expired record is a fresh process that never started; it is cleared so the next cut can begin
135
135
  // rather than being wedged behind a stale file the exclusive create would refuse forever.
@@ -200,20 +200,10 @@ async function finish_cut(target: string, lane: LaneInfo, request: CutRequest):
200
200
  if (refused !== undefined) return refused
201
201
 
202
202
  return lane_relaunch.is_openai_lane(lane)
203
- ? report(CUT_VERDICT, SUCCESS_EXIT_CODE)
203
+ ? report_relaunched_cut()
204
204
  : relaunch(target, lane, request.phase)
205
205
  }
206
206
 
207
- // The cut outside a lane is taken, and nothing is relaunched: the session that took it is the one a
208
- // person is watching, so the run is handed on the way `fullrun`'s entry `over` stop hands it on.
209
- function report_held_cut(issue: string): number {
210
- console.error(
211
- `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.`,
212
- )
213
-
214
- return report(CUT_VERDICT, SUCCESS_EXIT_CODE)
215
- }
216
-
217
207
  // **A `fullrun` held in its own checkout takes the implementation cut too**, so a run a person started
218
208
  // has a bound on its context mid-implementation. The hold naming this issue is what marks the run — a
219
209
  // tree nobody holds for it, and the pre-gate phase (which a held run's gate reaches in the same
@@ -228,8 +218,11 @@ async function cut_outside_lane(
228
218
 
229
219
  const state = await run_cut.current_state()
230
220
 
231
- if (state.held_issue !== request.issue) return report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE)
232
- if (!(await is_lane_branch(state))) return refuse_unready(state)
221
+ if (state.held_issue !== request.issue) {
222
+ return await declined(request, state, report(NOT_A_LANE_VERDICT, SUCCESS_EXIT_CODE))
223
+ }
224
+
225
+ if (!(await is_lane_branch(state))) return await declined(request, state, refuse_unready(state))
233
226
 
234
227
  clear_expired(target)
235
228
 
@@ -250,7 +243,10 @@ async function cut(
250
243
 
251
244
  const state = await run_cut.current_state()
252
245
 
253
- if (!(await is_lane_branch(state))) return refuse_unready(state)
246
+ if (!(await is_lane_branch(state))) {
247
+ return await declined({ issue, phase }, state, refuse_unready(state))
248
+ }
249
+
254
250
  if (skips_pre_gate_cut(phase)) return report_under_threshold()
255
251
 
256
252
  clear_expired(target)
@@ -0,0 +1,72 @@
1
+ import { stamp_file } from '#scripts/josh/stamp-file'
2
+ import { json_value } from '#scripts/lib/json-value'
3
+ import { z } from 'zod'
4
+ import { run_cut, type CutState } from './run-cut'
5
+
6
+ // The implementation cut a run asked for and was refused — `not-a-lane` or `unready` — recorded so the
7
+ // implementation-cut guard stops asking for it (joshuafolkken/kit#3881).
8
+ //
9
+ // **The reissue window alone re-asked every 90 seconds.** A run whose `run:cut --impl` can only answer
10
+ // `unready` was refused edit after edit, each refusal a round trip that cut nothing. Neither answer
11
+ // changes while the same hold stands over the tree, so the record names the issue and the hold it
12
+ // answered, and the guard stays silent while both still match; a fresh hold — a new run — asks again.
13
+
14
+ const DECLINE_PREFIX = 'josh-run-cut-decline-'
15
+
16
+ const decline_schema = z.object({ issue: z.string(), hold_taken_at: z.string().optional() })
17
+
18
+ type CutDecline = z.infer<typeof decline_schema>
19
+
20
+ function decline_path(git_directory: string): string {
21
+ return stamp_file.stamp_path(DECLINE_PREFIX, git_directory)
22
+ }
23
+
24
+ function record(git_directory: string, decline: CutDecline): void {
25
+ stamp_file.replace_stamp(decline_path(git_directory), decline)
26
+ }
27
+
28
+ function read(git_directory: string): CutDecline | undefined {
29
+ const raw = stamp_file.read_stamp_text(decline_path(git_directory))
30
+
31
+ return raw === undefined ? undefined : json_value.parse_with(raw, decline_schema)
32
+ }
33
+
34
+ function matches(recorded: CutDecline | undefined, current: CutDecline): boolean {
35
+ return recorded?.issue === current.issue && recorded.hold_taken_at === current.hold_taken_at
36
+ }
37
+
38
+ // The same read taken synchronously, for the `PreToolUse` guard. A checkout git cannot describe has no
39
+ // record, so the guard keeps speaking there.
40
+ function is_declined_sync(
41
+ issue: string,
42
+ directory: string | undefined = run_cut.worktree_git_directory_sync(),
43
+ ): boolean {
44
+ if (directory === undefined) return false
45
+
46
+ const hold_taken_at = run_cut.hold_of(directory)?.taken_at
47
+
48
+ return matches(read(directory), { issue, hold_taken_at })
49
+ }
50
+
51
+ // **An implementation cut `run:cut` refuses is recorded for the guard that asked for it**, then its
52
+ // exit code is passed back unchanged. The pre-gate phase has its own guard and records nothing.
53
+ async function declined(
54
+ request: { issue: string; phase: string },
55
+ state: CutState,
56
+ code: number,
57
+ ): Promise<number> {
58
+ if (request.phase !== run_cut.IMPLEMENTATION_PHASE) return code
59
+
60
+ const directory = await run_cut.worktree_directory()
61
+
62
+ if (directory !== undefined) {
63
+ record(directory, { issue: request.issue, hold_taken_at: state.hold_taken_at })
64
+ }
65
+
66
+ return code
67
+ }
68
+
69
+ const run_cut_decline = { decline_path, declined, is_declined_sync, read, record }
70
+
71
+ export type { CutDecline }
72
+ export { run_cut_decline }
@@ -0,0 +1,74 @@
1
+ import { UNTRACKED_FILES_FLAG } from '#scripts/git/constants'
2
+ import { git_command } from '#scripts/git/git-command'
3
+ import { git_diff_reads } from '#scripts/git/git-diff-reads'
4
+ import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
5
+ import type { CutState } from './run-cut'
6
+
7
+ // Whether a cut can be taken on this tree — the one predicate `run:cut` refuses `unready` on and the
8
+ // implementation-cut guard asks before it refuses an edit (joshuafolkken/kit#3881).
9
+ //
10
+ // **The guard and the command must not disagree.** The guard once fired on every over-threshold edit
11
+ // of a run held on the default branch, where `run:cut --impl` can only ever answer `unready`; each
12
+ // refusal cost a round trip that cut nothing. Reading the same predicate keeps the guard silent
13
+ // exactly where the command would refuse.
14
+
15
+ interface CutTree {
16
+ branch: string
17
+ default_branch: string
18
+ is_dirty: boolean
19
+ }
20
+
21
+ // A cut needs an uncommitted implementation on a branch other than the default one: the resume
22
+ // verifies both, so a tree missing either has nothing a fresh process could resume.
23
+ function is_ready(tree: CutTree): boolean {
24
+ return tree.branch !== tree.default_branch && tree.is_dirty
25
+ }
26
+
27
+ // The same predicate over the state `run:cut` reads, against the default branch asked asynchronously.
28
+ async function is_lane_branch(state: CutState): Promise<boolean> {
29
+ const default_branch = await git_command.get_default_branch()
30
+
31
+ return is_ready({ branch: state.branch, default_branch, is_dirty: state.is_dirty })
32
+ }
33
+
34
+ // `--branch` puts the branch on a `# branch.head` header line, so one call answers both halves; every
35
+ // other line is a change.
36
+ const STATUS_ARGUMENTS = ['status', '--porcelain=v2', '--branch', UNTRACKED_FILES_FLAG] as const
37
+ const HEADER_PREFIX = '#'
38
+ const BRANCH_HEADER = '# branch.head '
39
+
40
+ function tree_of(status: string, default_branch: string): CutTree | undefined {
41
+ const lines = status.split('\n').filter((line) => line !== '')
42
+ const header = lines.find((line) => line.startsWith(BRANCH_HEADER))
43
+
44
+ if (header === undefined) return undefined
45
+
46
+ const branch = header.slice(BRANCH_HEADER.length)
47
+ const is_dirty = lines.some((line) => !line.startsWith(HEADER_PREFIX))
48
+
49
+ return { branch, default_branch, is_dirty }
50
+ }
51
+
52
+ // The tree read synchronously, for the `PreToolUse` guard — `undefined` where git cannot answer.
53
+ function read_tree_sync(): CutTree | undefined {
54
+ const status = git_spawn_sync.read(STATUS_ARGUMENTS)
55
+
56
+ if (status === undefined) return undefined
57
+
58
+ const reference = git_spawn_sync.read(git_diff_reads.DEFAULT_BRANCH_ARGUMENTS)
59
+
60
+ return tree_of(status, git_diff_reads.default_branch_of(reference))
61
+ }
62
+
63
+ // **A tree git cannot describe reads as ready**, so the guard keeps speaking rather than going quiet
64
+ // on a read fault — the same direction `run_cut.carried_cut_sync` takes for an unreadable record.
65
+ function is_ready_sync(read: () => CutTree | undefined = read_tree_sync): boolean {
66
+ const tree = read()
67
+
68
+ return tree === undefined || is_ready(tree)
69
+ }
70
+
71
+ const run_cut_ready = { is_lane_branch, is_ready, is_ready_sync, tree_of }
72
+
73
+ export type { CutTree }
74
+ export { run_cut_ready }